h428 的博客

至人无己,神人无功,圣人无名!

0%

概述

今天要去看医生,因此提前整理了一下,老师和同学都觉得我有抑郁症的倾向,我自己觉得不是,我认为我自己就是看的太透了,一眼望到本质,导致一切都没了意义。

根据检查结果,显示有轻度抑郁症。

叠甲:我不是说下面我要说的这些看法一定都是对的,我只是说我是这么认为的。但我是个讲究君子和而不同的人,每个人都有每个人的看法,你可以有不同的意见。我从来只是自己思考,不去试图改变别人,当然我自己也有点犟,别人也很难改变我的认知。

辞职原因

我这个人就像是那种天生带有 4 个废物工作词条的帕鲁,其实从还没毕业还没开始工作,就在不断地幻想早点退休,要怎么提前退休,只不过现在提前到来了。

关于辞职原因,分析了一下,大概是 70% 个人原因,30% 工作原因。

个人原因

总结一句话就是:觉得人生没有意义,找不到活着的方向,只想先活在当下。

哲学:今年陆陆续续,看了不少人生哲学的内容,包括米格道、斯多葛、老庄、叔本华、尼采、萨特、加缪,思考量可以说比以前写代码时还要多,在这些内容的结合下,解构了非常多东西,觉得世界是荒诞的,一切都没有意义,人生没有意义,不知该何去何从。

悲观主义:我这个人本来就是天生的悲观主义者,和叔本华一样。

荒诞感:看了加缪,感觉世界是荒诞的,人生没有意义,觉得活在当下就好;尤其是看了《局外人》,真的从莫尔索身上看到一点自己的影子,只不过我没像他那么极端。

荒诞与自杀:但还没想过要自杀,虽然我思考过相关议题,但加缪也说荒诞并不能直接推导出自杀。

什么都不想做:然后很经常会有什么都不想做的状态,心很累。比如前两天情况比较严重时,就躺在草坪上一下午,看天空,听着歌,什么都不想做。

耗虫:耗虫长在人的心里,每天一睁开眼睛,就在不停地思索,躺床上也很难入睡,各种想法源源不断地自动在脑海里喷涌,根本控制不住。而且这不是简单一句“你别想那么多”能解决的,就像势能会自发转化为动能,风吹草就会动一样,我根本控制不住不去思考。

技术热情的消散:技术热情消散的主要是阶级叙事视角和性别叙事视角带给我的,导致我觉得就算技术再高也没什么意义,不过是在资本化的今天获得生存资料的一种手段罢了,你技术再好也不过都是完成了一种自我异化。在一个资本横行的女本位社会,只要你加入一种建构模式,你就在源源不断地被剥削,因此技术也没有什么意义。它剩下的唯一一点意义可能是让我能写点自己有用的小工具,比如原来用模拟器玩宝可梦时,实现了一个,能通过按手柄上的按键后,自动识别屏幕上的敌方宝可梦信息并检测出属性,以及建议派出什么属性的宝可梦去对战能打出双倍的伤害。

电子阳痿:但现在也对游戏不感兴趣了,都电子阳痿一年多了,黑猴出来也只打了一周就完全不想碰了。这在以前是完全不敢想的,我一年半之前都以为我能打一辈子游戏,现在就算有大量时间也懒得打游戏。以前的爱好可以说是打游戏和写代码,现在一个都不剩了。

口腹之欲:其实大概今年四月开始,我估计是已经有点下意识地意识到自己的情况,开始无意识地自救,下意识地用口腹之欲之类的世俗欲望去进行对冲,因此四月后开始变得爱出门,老想出去吃东西,也会开始花钱买一些东西。口腹之欲确实能短暂的麻痹自己,如果真的能持续地保持这种纯世俗的生活倒也不错,但很不幸,人类自从认知革命后,早已丧失了像动物那样完全活在当下的资格,人总是会不断地缅怀过去和畅想未来,在最基本的吃喝玩乐需求得到满足后,就会开始被所谓的意义感所包围,如果无法进一步满足就会产生虚无感。常人获得意义感的方式大概是世俗理想主义,比如封妻荫子,升职加薪,修身齐家治国平天下,流芳后世之类的东西,而基本已经将世俗理想主义完全解构的我,根本无法从这些中获得所谓的意义感。因此尽管纵享口腹之欲,也只是短暂麻痹自己。

狭隘的超世俗理想主义:对于常人,抛弃世俗理想主义后,还有一条比较简易的超世俗理想主义之路——宗教,毕竟宗教几千年来一直试图解决的就是人类在此岸世界的苦难,此岸世界如此操蛋,你只能将希望寄托于彼岸世界;但很不幸,我家从小就是信教的,我从小深谙此中之法,而我本身在思考问题时又是一个严格的唯物主义,故在看哲学时,已经把宗教之类的超世俗理想主义建构模式一起解构了,留下的只有个体宗教这一条狭隘的原子化路线。

爱好与精力、经济的矛盾:然后今年倒是陆陆续续体验过各种不同的爱好,骑行钓鱼摄影徒步登山乐器。这些倒都还行,除了钓鱼现在不怎么碰,其他都还继续碰碰,有的频繁有的不频繁,摄影还算比较感兴趣,确实在从事这些活动的期间能暂时麻痹自己。但在出去结束回到宿舍或工位之后,又会进一步陷入更深层次的虚无感中,简直就是叔本华钟摆理论的完美例子。还有就是人毕竟时间、精力有限,又有工作要完成,有时候真就没精力做这些爱好,这些就完全都不想碰,就想什么都不做,就呆在那发呆,我今年平均每天都稳定发呆好几个小时。

活在当下:既然人生没有意义,我只想先活在当下,按当下来讲,我的精神状态非常差,非常累,什么都不想做,只剩下思考。所以既然无法再推动工作这块巨石,我只能暂时不去管它,辞职休息。

工作原因

程序员变为联络员:我骨子里是个程序员,我以前其实是很喜欢写代码的。最想听到的任务大致是:这有个相对可行的方案,你去实现一下,不可行的地方讨论解决;或者这有个问题,是你技术栈能 cover 的问题,你出个方案,有需要我这边可以帮你协调资源。而不是这样的任务:你去联系一下,你写一下这份文档,你统计一下,你协调一下。这些东西我是非常厌恶的,毕竟每个人的意义感不同,对我个人来说我希望我的心神是耗费在一行行代码上,而不是写一份份一点意义都没有的文档和拟一则则需要格外注意措辞的通知,以及和外部人员联系时的戴上假面,维护所谓的人情世故。这些都极其耗费我的心神,现在回看我不敢相信居然能坚持这么久。

理想主义,眼高手低:其实最开始回实验室,是为了写火灾这个项目,我是真想真正去实现它,刚回来时我还特别认真,也开过几次会讨论尝试去努力设计和推进,但发现好像大家基本都不怎么在乎这事,久而久之我也没什么热情了,而且越了解越发现,整个项目不过是天马行空的想象罢了,很不具备可行性,没人知道怎么搞。而且我手上的活基本变成文档类的工作,久而久之也没有技术热情了。所以我其实可能并不适合这份工作。

直接原因:从知行合一角度讲,在意识到上述问题后,我应该第二天就提离职,那为什么到现在才辞职:主要还是没有契机,除了前面这些和我个人习惯不契合外,以世俗角度看来,这份工作其实是一份不错的工作,人际关系不那么复杂,我和实验室的师兄弟们处的也还好,大家关系都还行,整体上人际关系是比较愉悦的状态,也相对自由,所以这可能也是我能坚持这么久的原因,因为按我对自己的估计,我以前在公司写代码糊屎山,基本得保持工作一年休息半年的状态;最近恰好有了契机,最近组里办比赛,工作量大,如果只是工作量我应该都能 cover,但主要还是需要和不同的老师组织协调,一遇到这种活我就会心好累,在工作上我一般又都很难拒绝别人,都是偏向委屈自己,主要是懒得和别人扯皮和吵架,因此有矛盾也都自己消化,可你不能因为我能做就都推到我这边,本来不是我的工作职责范围的东西,虽然我完成了但心里肯定是不爽的,而且人毕竟抗压容量是有限的,这个比赛的各方协调就像是导火索一样导致以往累积的心理问题和以前思考的问题都一起爆发出来。说个比喻,就类似小说里修士在修行过程中会产生心魔,在渡劫时被引爆出来一样。

后续规划

后续规划:说实话,没想好,我目前只想也只能先活在当下,不想再缅怀过去和憧憬未来了。而且自从我丢失了玩游戏和写代码这两个在我之前的人生中最重要的两个活动后,我已经不敢相信自己的任何长远规划了,我没有信心。反正大致方向是先休息一段时间,让自己先恢复过来,然后再后续根据自身情况进一步规划;如果一直不恢复怎么办,暂时没想好,我只能把问题交给时间,只希望自己别真的走到连求存也解构掉的地步。

本月安排:我最多只能先做下这个月的安排:

  • 预计待到 11 月底回去,房子月底退了
  • 先把能卖的东西卖了,工作内容交接了
  • 这个月还是在厦门,把没去拍照的地方逛了,拍拍照

祝福

最后,攀登山顶的过程本身足以充实一个人的心灵,应当想象西西弗斯是幸福的,希望大家都能成为幸福的西西弗斯。

synchronized 介绍

synchronized 是由 JVM 规范定义的关键字,其为 Java 并发编程提供一种同步机制,使得在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。

在介绍 synchronized 的原理之前,我们先来看一下其常见的使用方式。在 Java 中,synchronized 主要有三种使用方式,分别为同步代码块、修饰静态方法和修饰实例方法,我们逐一来看相关用法。

同步代码块

同步代码块用法如下所示,提供任意一个 Object 对象给 synchronized 关键字作为同步资源(也叫“锁”,后续统称作为“锁”),只有获得该锁的线程才能执行内部代码。synchronized 为互斥锁,故任意时刻最多只能有一个线程获得该同步资源;同时 synchronized 为可重入锁,因此获得锁的线程可以在内部继续获得锁,对应的当然会多次释放锁。

1
2
3
synchronized(lock) {
// 内部代码块,任意时刻最多只能有一个线程执行
}

下面的 demo 展示了 synchronized 同步代码块的基本用法:每个线程调用一次 synchronizedCode 方法使 count 自增 1,最后打印 count 的最终结果,验证最终结果是否为线程数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 线程计数器
* 每个线程调用一次 synchronizedCode 使 count 自增 1,最后打印 count 结果
*/
public class Counter {

private int count = 0;

private final Object lock = new Object();

/**
* 以 lock 为锁,每执行一次 count 自增 1
*/
public void synchronizedCode() {
synchronized (lock) {
++count;
}
}

/**
* 对照组:不加锁的情况,会产生线程安全问题
*/
public void unSynchronizedCode() {
++count;
}

public void printCount() {
System.out.println("in the end, count = " + count);
}


public static void main(String[] args) throws InterruptedException {

// 线程数要设置大一点,太小了由于 cpu 太快可能看不出效果
int threadNum = 10000;
Thread[] threads = new Thread[threadNum];

System.out.println("测试未加 synchronized 关键字线程不安全的情况");
Counter testUnSync = new Counter();
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(testUnSync::unSynchronizedCode);
threads[i].start();
}
join(threads);
testUnSync.printCount();

System.out.println("测试添加 synchronized 关键字线程安全的情况");
Counter testSync = new Counter();
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(testSync::synchronizedCode);
threads[i].start();
}
join(threads);
testSync.printCount();
}

private static void join(Thread[] threads) {
try {
for (Thread thread : threads) {
thread.join();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

修饰静态方法

synchronized 可以直接修饰静态方法,由于静态方法为所有类实例所共有,因此其本质上相当于锁住了该类的 Class 对象,故下述两种用法本质上是等效的。

1
2
3
4
5
6
7
8
9
10
11
class Test {
public static synchronized void m1() {
// 同步代码
}

public static void m2() {
synchronized (Test.class) {
// 同步代码
}
}
}

修饰实例方法

synchronized 也可以直接修饰实例方法,当修饰实例方法时,其本质上相当于锁住了该实例的 this 对象,故下述中用法本质上是等效的。

1
2
3
4
5
6
7
8
9
10
11
class Test {
public synchronized void m1() {
// 同步代码
}

public void m2() {
synchronized (this) {
// 同步代码
}
}
}

补充内容

synchronized 是可重入锁,且必须是可重入锁,否则同一方法的递归,嵌套调用等将直接死锁。

程序中如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然可能发生不一致的情况。比如,在一个 web app 处理过程中,多个 Servlet 线程共同访问同一个资源,这是如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能访问到异常产生时的数据,因此要非常小心处理同步业务逻辑中的异常。

尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说。

前置知识

Java 对象内存布局与 Mark Word

在 JVM 中,Java 对象在内存中分为三块区域,分别是对象头、实例数据和字节对齐,其中对象头又包括 Mark Word 和类型指针 Klass Point。

  • 对象头:由 Mark Word 和 Klass Point 构成
    • Mark Word(标记字段):用于存储对象自身的运行时数据,例如存储对象的 HashCode,分代年龄、锁标志位等信息,是 synchronized 实现轻量级锁和偏向锁的关键。64 位 JVM 的 Mark Word 组成如下
      64 位 Mark Word
    • Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据:这部分主要是存放类的数据信息,父类的信息。
  • 字节对齐:为了内存的 IO 性能,JVM 要求对象起始地址必须是 8 字节的整数倍。对于不对齐的对象,需要填充数据进行对齐。

Moniterenter、Moniterexit 和 ACC_SYNCHRONIZED

对于 synchronized 的三种用法,我们使用下列代码观察其在字节码层面的特性。使用 javac -g:vars Main.java(-g:vars 是为了生成本地变量表)编译下列代码得到 Main.class,之后使用 javap -c -s -v -l Main.class 即可观察 JVM 字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public void test1() {
synchronized (new Main()) {

}
}

public synchronized void test2() {

}

public static synchronized void test3() {

}
}

对上述代码,得到的字节码如下,我们主要观察 test1(), test2() 和 test3() 三个方法中与 synchronized 关键字相关的字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
Classfile /C:/code/java/java-test/java8/src/main/java/Main.class
Last modified 202283日; size 424 bytes
SHA-256 checksum 224a2dd30cdc5ceb9bd056a48307bdf404c39868ad954f0c9776161e9878cadf
public class Main
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Main
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 4, attributes: 0
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Main
#8 = Utf8 Main
#9 = Methodref #7.#3 // Main."<init>":()V
#10 = Utf8 Code
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LMain;
#14 = Utf8 test1
#15 = Utf8 StackMapTable
#16 = Class #17 // java/lang/Throwable
#17 = Utf8 java/lang/Throwable
#18 = Utf8 test2
#19 = Utf8 test3
{
public Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMain;

public void test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: new #7 // class Main,创建 Main 对象并将引用放到 Operand Stack
3: dup // 复制一份 main 引用用于调用构造器
4: invokespecial #9 // Method "<init>":()V,消耗一份 main 引用调用构造器
7: dup // 再复制一份 main 引用
8: astore_1 // 将 main 引用存储到 LocalVariable[1](JVM 自动生成的局部变量,虽然在局部变量表中观查不到但确实存在)
9: monitorenter // 消耗一份栈顶的 main 引用,基于其所指向的对象获取 monitor 控制权
10: aload_1 // 读取 LocalVariable[1] 到栈顶,此处为 main 引用
11: monitorexit // 消耗一份栈顶的 main 引用,基于其所指向的对象释放 monitor 控制权
12: goto 20 // 跳转到 return
15: astore_2 // 异常部分字节码
16: aload_1
17: monitorexit
18: aload_2
19: athrow
20: return
Exception table:
from to target type
10 12 15 any
15 18 15 any
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this LMain;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 15
locals = [ class Main, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

public synchronized void test2();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // 修饰实例方法会生成 ACC_SYNCHRONIZED 标记
Code:
stack=0, locals=1, args_size=1
0: return
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LMain;

public static synchronized void test3();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // 修饰静态方法会生成 ACC_SYNCHRONIZED 标记
Code:
stack=0, locals=0, args_size=0
0: return
}

通过观察上述三个方法的字节码以及进一步的分析源码,我们可以得出下述结论:

  • 同步代码:通过 moniterenter 和 moniterexit 关联到到一个 monitor 对象,进入时设置 Owner 为当前线程,计数 +1、退出 -1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
  • 实例方法:为方法生成 ACC_SYNCHRONIZED 标记,会隐式调用 moniterenter 和 moniterexit,本质仍然是对象监视器 monitor 的获取
  • 静态方法:为方法生成 ACC_SYNCHRONIZED 标记,会隐式调用 moniterenter 和 moniterexit

其中 monitorenter 和 monitorexit 这两个 jvm 指令,是 JVM 对高级同步原语 monitor 支持的一种体现,在 Java 中可以通过结合使用 synchronized 关键字以及 Object 的 wait/notify 来使用这种 monitor 机制。

Java 中的 Monitor 机制

monitor 的概念

管程,英文是 Monitor,也常被翻译为“监视器”,monitor 不管是翻译为“管程”还是“监视器”,都是比较晦涩的,通过翻译后的中文,并无法对 monitor 达到一个直观的描述。

《浅析操作系统同步原语》 这篇文章中,介绍了操作系统在面对 进程/线程 间同步的时候,所支持的一些同步原语,其中 semaphore 信号量 和 mutex 互斥量是最重要的同步原语。

在使用基本的 mutex 进行并发控制时,需要程序员非常小心地控制 mutex 的 down 和 up 操作,否则很容易引起死锁等问题。为了更容易地编写出正确的并发程序,所以在 mutex 和 semaphore 的基础上,提出了更高层次的同步原语 monitor,不过需要注意的是,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持 monitor,Java 语言支持 monitor。

一般的 monitor 实现模式是编程语言在语法上提供语法糖,而如何实现 monitor 机制,则属于编译器的工作,Java 就是这么干的。

monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。但仅仅有互斥的作用是不够的,无法进入 monitor 临界区的 进程/线程,它们应该被阻塞,并且在必要的时候会被唤醒。显然,monitor 作为一个同步工具,也应该提供这样的管理 进程/线程 状态的机制。想想我们为什么觉得 semaphore 和 mutex 在编程上容易出错,因为我们需要去亲自操作变量以及对 进程/线程 进行阻塞和唤醒。monitor 这个机制之所以被称为“更高级的原语”,那么它就不可避免地需要对外屏蔽掉这些机制,并且在内部实现这些机制,使得使用 monitor 的人看到的是一个简洁易用的接口。

monitor 基本元素

monitor 机制需要几个元素来配合,分别是:

  • 临界区
  • monitor 对象及锁
  • 条件变量以及定义在 monitor 对象上的 wait,signal 操作。

使用 monitor 机制的目的主要是为了互斥进入临界区,为了做到能够阻塞无法进入临界区的 进程/线程,还需要一个 monitor object 来协助,这个 monitor object 内部会有相应的数据结构,例如列表,来保存被阻塞的线程;同时由于 monitor 机制本质上是基于 mutex 这种基本原语的,所以 monitor object 还必须维护一个基于 mutex 的锁。

此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。

Java 语言对 monitor 的支持

monitor 是操作系统提出来的一种高级原语,但其具体的实现模式,不同的编程语言都有可能不一样。以下以 Java 的 monitor 为例子,来讲解 monitor 在 Java 中的实现方式。

临界区界定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Monitor {

private Object ANOTHER_LOCK = new Object();

private synchronized void fun1() {
}

public static synchronized void fun2() {
}

public void fun3() {
synchronized (this) {
}
}

public void fun4() {
synchronized (ANOTHER_LOCK) {
}
}
}

实际上,被 synchronized 关键字修饰的方法、代码块,就是 monitor 机制的临界区。

monitor object

可以发现,上述的 synchronized 关键字在使用的时候,往往需要指定一个对象与之关联,例如 synchronized(this),或者 synchronized(ANOTHER_LOCK),synchronized 如果修饰的是实例方法,那么其关联的对象实际上是 this,如果修饰的是类方法,那么其关联的对象是 Xxx.class。总之,synchronzied 需要关联一个对象,而这个对象就是 monitor object。

monitor 的机制中,monitor object 充当着维护 mutex 以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。Java 语言中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 monitor object。

Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:
ObjectMonitor 模式

当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 Wait Set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。

synchronized 关键字的底层原理

synchronized 关键字本质上就是 JVM 层面对 monitor 机制的实现的一个关键元素之一,synchronized 关键字和 Object 的 wait/notify 方法共同组成了 Java 的 monitor 机制,它们的概念可以和 monitor 基本元素一一对应。

  • 临界区:使用 synchronized 修饰的内部就是临界区,只有一个线程可以进入
  • monitor object 和锁:synchronized 中的锁对象对应的就是 monitor 机制中的 monitor object 的概念,在 JVM 中有一个用 C++ 编写的 ObjectMonitor 用于辅助 Object 实现 Java 中的 monitor 机制。synchronized 在编译为字节码后体现为 monitorenter 和 monitorexit,最终会由 ObjectMonitor 辅助锁对象 Object 完成对临界区的互斥访问,ObjectMonitor 内部会维护一个 mutex 锁用于控制互斥访问临界区。
  • monitor object 上的 wait/signal 操作:对应到 Object 类的 wait(), notify(), notifyAll() 三个函数,同样由 ObjectMonitor 辅助 Object 实现,ObjectMonitor 会维护 Entry Set 和 Wait Set 来维护未拿到锁的线程,并按照一定的逻辑使其中一个获得锁。

网上很多文章以及资料,在分析 synchronized 的原理时,基本上都会说 synchronized 是基于 monitor 机制实现的,但很少有文章说清楚,都是模糊带过。

参照前面提到的 Monitor 的几个基本元素,如果 synchronized 是基于 monitor 机制实现的,那么对应的元素分别是什么?它必须要有临界区,这里的临界区我们可以认为是对对象头 mutex 的 P 或者 V 操作,这是个临界区,那 monitor object 对应哪个呢?mutex?总之无法找到真正的 monitor object。

所以我认为“synchronized 是基于 monitor 机制实现的”这样的说法是不准确的。Java 提供的 monitor 机制,其实是 Object,synchronized 等元素合作形成的,甚至说外部的条件变量也是个组成部分。JVM 底层的 ObjectMonitor 只是用来辅助实现 monitor 机制的一种常用模式,但大多数文章把 ObjectMonitor 直接当成了 monitor 机制。

我觉得应该这么理解:Java 对 monitor 的支持,是以机制的粒度提供给开发者使用的,也就是说,开发者要结合使用 synchronized 关键字,以及 Object 的 wait/notify 等元素,才能说自己利用 monitor 的机制去解决了一个生产者消费者的问题。

注意上述讨论的是重量级锁的底层原理,在 JDK 1.6 之前,synchronized 只有传统的锁机制,直接关联到 monitor 对象,本质上使用的是操作系统底层的 mutex 锁,而在 JDK 1.6 以后 JVM 对 synchronized 做了优化。

JDK1.6 之后的 synchronized 关键字底层做了哪些优化?

在 JDK 1.6 之前,synchronized 只有传统的锁机制,直接关联到 monitor 对象,存在性能上的瓶颈。在 JDK 1.6 后,为了提高锁的获取与释放效率,JVM 引入了两种锁机制:偏向锁和轻量级锁。它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。这几种锁的实现和转换正是依靠对象头中的 Mark Word 来标记不同的锁状态的,这也使得 synchronized 有一个锁升级的过程。

锁状态

前面已经介绍过 Java 对象的 Mark Word,在 Mark Word 中,JVM 采用最后三个 bit 用于标记锁状态,共有无锁、偏向锁、轻量级锁、重量级锁四种,且存在锁状态的升级过程。
64 位 Mark Word

此外,还有一个特殊的状态,叫匿名可偏向状态,其属于偏向锁的一种特殊情况,该状态下 Mark Word 的最后三位为 101 表示处于偏向锁状态,但前面的线程 ID 为 0 表示此时并未有线程持有锁。在偏向锁延迟时间(默认 4000 毫秒)结束后,JVM 对所有新建的对象,其 Mark Word 默认都处于匿名可偏向状态 0x0000000000000005

我们使用 openjdk 官网提供 jol-core 工具可以观察 Java 的对象头,以便我们更好地分析锁状态。其中 0.14 版本会以二进制形式打印对象头,而 0.16 版本以十六进制打印对象头,并会直接打印当前锁状态。

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>

偏向锁延迟(无锁)与匿名偏向

我们使用下述代码观察未加锁的情况下,锁对象的 Mark Word

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("JVM 刚启动时,锁对象的 Mark Word(无锁状态, ...001)");
printMarkWord(new Object());

Thread.sleep(4000L);
System.out.println("\n偏向锁延时初始化结束后,新创建锁对象的 Mark Word(匿名可偏向, 000000...101)");
printMarkWord(new Object());

}

private static void printMarkWord(Object o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

// 会打印如下结果
JVM 刚启动时,锁对象的 Mark Word(无锁状态)
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


偏向锁延时初始化结束后,新创建锁对象的 Mark Word(匿名可偏向)
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,不同时间点创建的锁对象,它们 Mark Word 有不同的值。最开始时创建的对象处于无锁状态 0x0000000000000001,休眠 4 秒后新建的对象为处于匿名可偏向状态 0x0000000000000005。这是因为偏向锁是延时初始化的,默认是 4000ms,初始化后会将所有加载的 Klass 的 prototype header 修改为匿名偏向样式。当创建一个对象时,会通过 Klass 的 prototype_header 来初始化该对象的对象头。

简单的说,默认只有在 JVM 启动后的最初 4000 毫秒内,新建的对象会处于无锁状态,当偏向锁初始化结束后,后续所有新建对象的对象头都为匿名可偏向状态。

为什么需要延迟初始化?JVM 启动时必不可免会有大量 synchronized 的操作,而偏向锁并不是都有利。如果开启了偏向锁,会发生大量锁撤销和锁升级操作,大大降低 JVM 启动效率。

此外,只有锁对象处于匿名偏向状态,线程才能拿到到我们通常意义上的偏向锁。对于无锁状态的锁对象,如果尝试获取锁(不管是否多线程争用),都会直接进入轻量级锁状态。因此如下代码所示,在 JVM 启动前 4 秒,如果尝试获取锁,会直接进入轻量级锁状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {
private final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("JVM 刚启动时,锁对象的 Mark Word(无锁状态, ...001)");
printMarkWord(lock);

synchronized (lock) {
System.out.println("\n在 JVM 启动的前 4000 ms 内尝试获得锁,所对象的 Mark Word(轻量级锁, threadId...00)");
printMarkWord(lock);
}
}

private static void printMarkWord(Object o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

JVM 刚启动时,锁对象的 Mark Word(无锁状态, ...001
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


在 JVM 启动的茜 4000 ms 内尝试获得锁,所对象的 Mark Word(轻量级锁, threadId...00
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000013b9bff0c8 (thin lock: 0x00000013b9bff0c8)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁

偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后若加锁则默认会进入轻量级锁状态。

前面已经介绍过,只有处于匿名可偏向状态的对象才能进入偏向锁模式,因此为了测试偏向锁,我们需要先休眠 4000 ms 再创建锁对象,或者修改启动时的 VM 参数,添加 -XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

加锁流程

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。其加锁流程大致如下:

  • 步骤 1、从当前线程的栈(Interpreted frames)中找到一个空闲的 Lock Record,并指向当前锁对象。
  • 步骤 2、获取对象的 markOop 数据 mark,即对象头的 Mark Word;
  • 步骤 3、判断锁对象的 mark word 是否是偏向模式,即低 3 位是否为 101。若不是,进入步骤 4。若是,计算 anticipated_bias_locking_value,判断偏向状态:
  • 步骤 3.1、anticipated_bias_locking_value 若为 0,代表偏向的线程是当前线程且 mark word 的 epoch 等于 class 的 epoch,这种情况下直接执行同步代码块,什么都不用做。
  • 步骤 3.2、判断 class 的 prototype_header 是否为非偏向模式。若为非偏向模式,CAS 尝试将对象恢复为无锁状态。无论 CAS 是否成功都会进入轻量级锁逻辑。
  • 步骤 3.3、如果 epoch 偏向时间戳已过期,则需要重偏向。利用 CAS 指令将锁对象的 mark word 替换为一个偏向当前线程且 epoch 为类的 epoch 的新的 mark word。
  • 步骤 3.4、CAS 将偏向线程改为当前线程,如果当前是匿名偏向(即对象头中的 bit field 存储的 Thread ID 为空)且无并发冲突,则能修改成功获取偏向锁,否则进入锁升级的逻辑。
  • 步骤 4、走到一步会进行轻量级锁逻辑。构造一个无锁状态的 Displaced Mark Word,然后存储到 Lock Record。设置为无锁状态的原因是:轻量级锁退出同步代码块时需要将对象头的 Mark Word 使用 CAS 替换为无锁状态。如果是锁重入,则将 Lock Record 的 Displaced Mark Word 设置为 null,放到栈帧中,起到计数作用。

偏向锁加锁流程

步骤 1 中提到了 Lock Record,其是分配在线程的 Interpreted frames 上的一块区域(可以简单地看成是 List<LockRecord>),该区域保存了该线程所有已分配的 Lock Record,而 Lock Record 又指向锁对象,故可以通过遍历该区域知道当前线程占用了哪些锁

Interpreted frames contain a region which holds the lock records for all monitors owned by theactivation. During interpreted method execution this region grows or shrinks depending upon the number of locks held.

释放流程

在持有偏向锁的线程退出同步代码块后,会触发偏向锁的释放。偏向锁的释放可参考bytecodeInterpreter.cpp#1923。偏向锁的释放只要将对应 Lock Record 释放就好了,但这里的释放并不会将 mark word 里面的 thread ID 去掉,这样做是为了下一次更方便的加锁。而轻量级锁则需要将首个 Displaced Mark Word 替换到对象头的 mark word 中。如果 CAS 失败或者是重量级锁则进入到 InterpreterRuntime::monitorexit 方法中。

撤销流程

在退出同步块后,持有偏向锁的线程虽然释放了锁(移除了偏向锁的 Lock Record),但锁对象的 threadId 仍然保留为原有偏向线程没有清除,故该释放操作对其他线程是不可感知的。当遇到其他线程尝试获取偏向锁时,会触发撤销偏向锁并升级为轻量级锁。

偏向锁的撤销(revoke)是一个很特殊的操作,为了执行撤销操作,需要等待全局安全点(即 STW,在这个时间点上没有字节码正在执行,引用关系不会发生变化),它会首先暂停拥有偏向锁的线程,判断该线程是否持有锁,将锁对象设置为无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。其具体步骤如下:

  • 步骤 1、查看偏向的线程是否存活,如果已经死亡,则直接撤销偏向锁。JVM 维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。
  • 步骤 2、偏向的线程是否还在同步块中,如果不在,则撤销偏向锁变为无锁(对象头变为 01,相当于是轻量级锁但退出同步代码块)。如果在同步块中,执行步骤 3。这里是否在同步块的判断基于上文提到的偏向锁的重入计数方式:在偏向锁的获取中,每次进入同步块的时候都会在栈中找到第一个可用(即栈中最高的)的 Lock Record,将其 obj 字段指向锁对象,每次解锁的时候都会把最低的 Lock Record 移除掉,所以可以通过遍历线程栈中的 Lock Record 来判断是否还在同步块中。轻量级锁的重入也是基于 Lock Record 的计数来判断。
  • 步骤 3、升级为轻量级锁。将偏向线程所有相关 Lock Record 的 Displaced Mark Word 设置为 null,再将最高位的 Lock Record 的 Displaced Mark Word 设置为无锁状态,然后将对象头指向最高位的 Lock Record。这里没有用到 CAS 指令,因为是在 safepoint,可以直接升级成轻量级锁。

需要特别注意,经测试,偏向锁只有首个尝试加锁的线程才能进入,只要有其他线程尝试获取锁,尽管原有偏向线程已经退出同步代码块,但 threadId 仍然为原有偏向线程且对其他线程是不可感知的,故其他线程尝试获取锁时,此时理论上虽然没有争用,但仍然会直接膨胀为轻量级锁,即偏向锁只会偏向首个线程,不可重偏向至其他线程。(代码验证的结果,是否 100% 正确有待商榷)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class Main {
private final static Object lock = new Object();

private static void tryLock() {
synchronized (lock) {
Thread t = Thread.currentThread();
System.out.println(t.getName() + "-" + t.getId() + " 尝试获取到锁,Mark Word 为:");
printMarkWord(lock);
}
}

// -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {

// 定义子线程尝试获取锁,由于无争用,获取到偏向锁
Thread child = new Thread(() -> {
tryLock();
}, "child");
child.start();

// 子线程结束后再休眠三秒,打印并确认子线程结束(状态为 TERMINATED)
child.join();
Thread.sleep(3000L);

System.out.println("child 子线程状态为 " + child.getState());
System.out.println();

// 此时打印锁对象的 Mark Word,确认仍然为偏向锁,且 threadId 并未被替换
// 即虽然退出了同步区并释放了锁,但 Mark Word 仍然体现为偏向锁
System.out.println("子线程 child 结束后,lock 的 Mark Word(仍然保留偏向锁 101):");
printMarkWord(lock);

// 此时,主线程获取锁,虽然实际上同步区已经结束,理论上没有争用,
// 但由于 Mark Word 的 threadId 仍然为子线程 id,故也会升级为轻量级锁
// 故实际上偏向锁只能在第一次加锁的线程处偏向一次,即使线程结束也无法撤销,
// 之后若再有其他线程尝试获取锁,则会撤销偏向锁并升级为轻量级锁
System.out.println("子线程 child 结束后,主线程尝试加锁时的 Mark Word(撤销偏向锁升级为轻量级锁 00)");
tryLock();

System.out.println("轻量级锁释放后,锁对象的 Mark Word(变回无锁状态 01)");
printMarkWord(lock);
}

private static void printMarkWord(Object o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}


child-12 尝试获取到锁,Mark Word 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000002447d0a9805 (biased: 0x00000000911f42a6; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

child 子线程状态为 TERMINATED

子线程 child 结束后,lock 的 Mark Word(仍然保留偏向锁 101):
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000002447d0a9805 (biased: 0x00000000911f42a6; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

子线程 child 结束后,主线程尝试加锁时的 Mark Word(撤销偏向锁升级为轻量级锁 00
main-1 尝试获取到锁,Mark Word 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000b798cff560 (thin lock: 0x000000b798cff560)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

轻量级锁释放后,锁对象的 Mark Word(变回无锁状态 01
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

轻量级锁与无锁

加锁与释放流程

当偏向锁出现多线程争用时,就会膨胀为轻量级锁。其加锁流程如下:

  • 在代码访问同步资源时,如果锁对象处于无锁不可偏向状态,jvm 首先将在当前线程的栈帧中创建一条锁记录(lock record),用于存放:
    • displaced mark word(置换标记字):存放锁对象当前的 mark word 的拷贝
    • owner 指针:指向当前的锁对象的指针,在拷贝 mark word 阶段暂时不会处理它
      偏向锁 Lock Record
  • 在拷贝 mark word 完成后,首先会挂起线程,jvm 使用 CAS 操作尝试将对象的 mark word 中的 lock record 指针指向栈帧中的锁记录,并将锁记录中的 owner 指针指向锁对象的 mark word
  • 如果 CAS 替换成功,表示竞争锁对象成功,则将锁标志位设置成 00,表示对象处于轻量级锁状态,执行同步代码中的操作
  • 如果 CAS 替换失败,则判断当前对象的 mark word 是否指向当前线程的栈帧:
    • 如果是则表示当前线程已经持有对象的锁,执行的是 synchronized 的锁重入过程,可以直接执行同步代码块
    • 否则说明该其他线程已经持有了该对象的锁,如果在自旋一定次数后仍未获得锁,那么轻量级锁需要升级为重量级锁,将锁标志位变成 10,后面等待的线程将会进入阻塞状态

轻量级锁的释放:最后一次(重入时)退出代码块后,使用 CAS 操作,尝试将 displaced mark word 替换回 mark word,这时需要检查锁对象的 mark word 中 lock record 指针是否指向当前线程的锁记录

  • 如果替换成功,则表示没有竞争发生,整个同步过程就完成了
  • 如果替换失败,则表示当前锁资源存在竞争,有可能其他线程在这段时间里尝试过获取锁失败,导致自身被挂起,并修改了锁对象的 mark word 升级为重量级锁,最后在执行重量级锁的解锁流程后唤醒被挂起的线程

偏向锁流程

重入实现

可重入锁 Lock Record

参考链接

1. 测试标题和段落:《春》

1.1 作品原文

盼望着,盼望着,东风来了,春天的脚步近了。

一切都像刚睡醒的样子,欣欣然张开了眼。山朗润起来了,水涨起来了,太阳的脸红起来了。

小草偷偷地从土里钻出来,嫩嫩的,绿绿的。园子里,田野里,瞧去,一大片一大片满是的。坐着,躺着,打两个滚,踢几脚球,赛几趟跑,捉几回迷藏。风轻悄悄的,草软绵绵的。

桃树、杏树、梨树,你不让我,我不让你,都开满了花赶趟儿。红的像火,粉的像霞,白的像雪。花里带着甜味儿;闭了眼,树上仿佛已经满是桃儿、杏儿、梨儿。花下成千成百的蜜蜂嗡嗡地闹着,大小的蝴蝶飞来飞去。野花遍地是:杂样儿,有名字的,没名字的,散在草丛里,像眼睛,像星星,还眨呀眨的。

“吹面不寒杨柳风”,不错的,像母亲的手抚摸着你。风里带来些新翻的泥土的气息,混着青草味儿,还有各种花的香,都在微微润湿的空气里酝酿。鸟儿将窠巢安在繁花嫩叶当中,高兴起来了,呼朋引伴地卖弄清脆的喉咙,唱出宛转的曲子,与轻风流水应和着。牛背上牧童的短笛,这时候也成天嘹亮地响着。

雨是最寻常的,一下就是三两天。可别恼。看,像牛毛,像花针,像细丝,密密地斜织着,人家屋顶上全笼着一层薄烟。树叶儿却绿得发亮,小草儿也青得逼你的眼。傍晚时候,上灯了,一点点黄晕的光,烘托出一片安静而和平的夜。在乡下,小路上,石桥边,有撑起伞慢慢走着的人,地里还有工作的农民,披着蓑戴着笠。他们的房屋,稀稀疏疏的在雨里静默着。

天上风筝渐渐多了,地上孩子也多了。城里乡下,家家户户,老老小小,也赶趟儿似的,一个个都出来了。舒活舒活筋骨,抖擞抖擞精神,各做各的一份事去。“一年之计在于春”,刚起头儿,有的是工夫,有的是希望。

春天像刚落地的娃娃,从头到脚都是新的,它生长着。

春天像小姑娘,花枝招展的,笑着,走着。

春天像健壮的青年,有铁一般的胳膊和腰脚,领着我们上前去

1.2 创作背景

该文创作时间大约在 1933 年间。此时作者朱自清刚刚结束欧洲漫游回国,与陈竹隐女士缔结美满姻缘,而后喜得贵子,同时出任清华大学中国文学系主任,人生可谓好事连连,春风得意。

1.3 作品鉴赏

1.3.1 主题思想

该文的主题思想即对自由境界的向往。朱自清当时虽置身在污浊黑暗的旧中国,但他的心灵世界则是一片澄澈明净,他的精神依然昂奋向上。朱自清把他健康高尚的审美情趣,把他对美好事物的无限热爱,将他对人生理想的不懈追求熔铸到文章中去。熔铸到诗一样美丽的语言中去。从而使整篇文章洋溢着浓浓的诗意,产生了经久不衰的艺术魅力。

《春》——在这篇“贮满诗意”的“春的赞歌”中,事实上饱含了作家特定时期的思想情绪、对人生及至人格的追求,表现了作家骨子里的传统文化积淀和他对自由境界的向往。1927 年之后的朱自清,始终在寻觅着、营造着一个灵魂深处的理想世界——梦的世界,用以安放他“颇不宁静”的拳拳之心,抵御外面世界的纷扰,使他在幽闭的书斋中“独善其身”并成就他的治学。《春》描写、讴歌了一个蓬蓬勃勃的春天,但它更是朱自清心灵世界的一种逼真写照。朱自清笔下的“春景图”,不是他故乡江浙一带的那种温暖潮湿的春景,也不是北方城郊的那种壮阔而盎然的春景,更不是如画家笔下那种如实临摹的写生画,而是作家在大自然的启迪和感召下,由他的心灵酿造出来的一幅艺术图画。在这幅图画中,隐藏了他太多的心灵密码。

1.3.2 写作手法

朱自清的散文《春》充满了叙不完的诗情、看不尽的画意。他将人格美的“情”与自然美的“景”水乳交融在一起,创造了情与景会、情景交融的艺术境界。朱自清在这篇仅仅 30 个句子的简短散文中。运用了二十多处修辞手法,频率之高,令人惊诧。作品是以“春”贯穿全篇,由盼春、绘春、颂春三个部分组成,逐层深入、环环相扣。而作者正是以修辞格来作为《春》的“颜料”,淋漓尽致地描绘出那幅五彩缤纷的早春图。

1、殷切盼春归

“盼望着,盼望着,东风来了,春天的脚步近了。”作品一开头,作者就用了一个反复修辞格。“盼望”这一动词的反复使用。突兀、有力、急切地反映出人们期盼春天来临的迫切心情。紧接着,用一个“拟人”辞格,来传递春天的讯息。春,是人们所心仪的,是可感可知的,可爱可亲的。春天的脚步声,更是人们极为熟悉的。来了,近了,它是人们在历经三九寒冬之后所殷切期盼的。在此。作者写出了人们对春天的翘首企盼之情和迎接春天的万分欣喜之情。 [4]

2、热情绘春景

“一切都像刚睡醒的样子,欣欣然张开了眼”用了“拟人”辞格。在作者的笔下,春风轻拂,大地回暖,万物复苏,仿佛一个“刚睡醒”的人,“欣欣然张开了眼”。初春,好一种淡淡的气息;初春,好一派朦胧的景象。“山朗润起来了,水涨起来了,太阳的脸红起来了。”其中,“太阳的脸红起来了”用了“拟人”辞格,将太阳人格化,既抓住了春天太阳的特征,表现了春阳的温暖,更展示出春阳内在的神韵。整个句子又构成排比句,“拟人”、“排比”的套用,从大处着笔,对山、水、太阳进行了粗线条的描画,简明地勾勒出初春的总轮廓。为下文深层次、多视角地描绘春景图做铺垫。尤其值得一提的是,朱自清用“朗润”描写的山,使山富有光泽、格外的洒脱。

“小草偷偷地从土里钻出来,嫩嫩的,绿绿的”用了“叠音”和“拟人”修辞格。“偷偷”、“钻”等词语将小草顽强的生命力传神地表现出来,正所谓,“一岁一枯”,“野火烧不尽,春风吹又生。”而这也象征了人类社会世世代代繁衍生息,且总是向着更美好、更高级的社会进化、演变。“园子里,田野里,瞧去,一大片一大片满是的”用的是“反复”(重复)修辞格。嫩绿的小草“一大片一大片”的,长满了园子和田野,视线所及之处都是这绿的世界,让读者感受到这春草绿得多么诱人,而且具有很强的层次感。“坐着,躺着,打两个滚,踢几脚球,赛几趟跑,捉几回迷藏”用的是“排比”修辞格。值此大地回暖时节。人们告别封冻了一冬的粉妆玉砌的世界,来到满是绿色的草坪“坐着,躺着”,沐浴着春阳,甚是惬意。和着和煦的微风,开展各种户外活动。锻炼身体,增强体质。使人得以保持精神饱满的状态。
“桃树、杏树、梨树,你不让我,我不让你,都开满了花赶趟儿”是“排比”、“连环”及“拟人”几种修辞格连用,将桃花、杏花、梨花的竞相开放描绘得非常生动、非常形象。“红的像火,粉的像霞,白的像雪”,将三个“比喻”修辞格连着使用。而这三个比喻句又组成排比句。作者从色彩的角度,将桃花、杏花、梨花描绘得多姿多彩,鲜艳夺目,而且非常逼真。确是花卉争荣,各不相让。这些个花儿,充满了生命的芬芳,也使整幅春景图的色彩更为丰富、润泽。

“花里带着甜味,闭了眼,树上仿佛已经满是桃儿、杏儿、梨儿”用的是“通感”和“排比”修辞格。“花”是视觉,作者把它移植到味觉,说是“带着甜味”。看着春华想到秋实——满树的“桃儿、杏儿、梨儿”,着实让人过足了喜获水果丰收之瘾。这样的想象不仅拓宽了描绘的视野,更从另一角度渲染了春花的可爱。“花下成千成百的蜜蜂嗡嗡的闹着。大小的蝴蝶飞来飞去”用的“拟人”修辞格。一个“闹”字。将蜜蜂人格化,非常贴切。这样的描写既表现出声响。隐含着一片喧闹沸腾,更寓意着一派春意盎然、生机勃勃的景象。“野花遍地是:杂样儿,有名字的,没名字的,散在草丛里,像眼睛,像星星,还眨呀眨的”是“比喻”的连用及“比喻”、“拟人”修辞格的套用。草丛里的野花“像眼睛,像星星,还眨呀眨的”,非常生动。正是这些小野花,与别的花儿一起组成春花大家族,将春天大地装扮得分外靓丽妖绕。

“‘吹面不寒杨柳风’,不错的,像母亲的手抚摸着你”是“引用”与“比喻”修辞格的套用。句子先引用了南宋志南和尚的诗句,用以状写春风的温暖、柔和,非常亲切可感。

春风“像母亲的手抚摸着你”用了“比喻”修辞格,这个比喻让人觉得非常亲切、非常生活化,容易勾起人们儿时的回忆,倍感母爱的温暖和伟大。“鸟儿将窠巢安在繁花嫩叶当中,高兴起来了,呼朋引伴地卖弄清脆的喉咙,唱出宛转的曲子,与轻风流水应和着。”此句用的是“拟人”修辞格。鸟儿都来“卖弄”歌喉,它们宛转的曲子“与轻风流水应和着”。作者以“鸟唱”等鸟儿欢快的表现,衬托出人们愉悦的心情,反映出春天给人们、鸟儿、大地上的一切生灵带来了欢愉。
“看,像牛毛,像花针,像细丝,密密地斜织着,入家屋顶上全笼着一层薄烟”用了“比喻”、“排比”和“拟人”修辞格。作者将连绵春雨比作牛毛、花针、细丝,这三个比喻连用构成了排比。接着,用一个“织”字,将春雨人格化,也将春雨描绘得异常的湿润。“树叶子却绿得发亮,小草也青得逼你的眼”是宽式的(非严格意义的)“对偶”。作者通过这种修辞手法,加深了春景图中树叶的“绿”和小草的“青”,使整幅图更加浓墨重彩。图中所描绘的树、草及其它植物,都呈现出一派生机和活力。

“乡下去,小路上,石桥边,撑起伞慢慢走着的人;还有地里工作的农夫,披着蓑,戴着笠的。”其中,“小路上,石桥边”“披着蓑.戴着笠”用的是“对偶”修辞格,将乡间的各式人等的活动描绘出来。撑起伞,走在小路上、石桥边的入,心情放松.正慢慢地体会着初春的细雨“斜织”;而农夫则为了当年的好收成,借着大好的春光,“披着蓑,戴着笠”在地里忙着。

“他们的房屋,稀稀疏疏的,在雨里静默着”用了“叠音”和“拟人”修辞格。此处的“叠音”体现了声音美和语感美。房屋“在雨里静默着”是将房屋人格化,将蛰伏了一冬的房屋描绘得更富有灵性,装点着烟雨初春的乡村。
“城里乡下,家家户户,老老小小,他们也赶趟儿似的,一个个都出来了”共用了三个“叠音”修辞。作者通过声音的繁复增进语感的繁复,借声音的和谐增强语调的和谐。

“舒活舒活筋骨,抖擞抖擞精神,各做各的一份事去”用了两个“反复”修辞格和一个“对偶”修辞格。“舒活”、“抖擞”两个词语的重复出现使前两个语段构成“排比”,有意识地突出“蛰伏”了一冬的人们不愿辜负大好的春光,正大步迈进春天,以十二分的热情,聚集十二万分的潜能,全身心地投入到各项工作中。“‘一年之计在于春’,刚起头儿,有的是工夫,有的是希望”是“引用”修辞格和“双关”修辞格的套用。农民抓住农时,忙于春耕春种,以使来年衣食无忧。其他行业的人们也抓住春天这一大好时机,通过一番努力,实现生活的美好愿景。

朱自清在贴近大自然、感悟大自然、描绘大自然的同时,赞美那些在大好春光里辛勤劳作、奋然向前的人们的思想情感。

3、春之礼赞

“春天像刚落地的娃娃,从头到脚都是新的。它生长着”将春天比作“娃娃”,是“比喻”和“拟人”兼用。春天原非像别的事物那样可知、可感、可触摸,但作者把它比作新生的娃娃后,就赋予了它新的生命。

“春天像小姑娘,花枝招展的,笑着,走着。”此旬兼用“比喻”和“拟人”,将春天比作“小姑娘”。春天渐渐长大,变成“花枝招展的”小姑娘。她亭亭玉立,落落大方,“笑着,走着”,着实招人喜爱。社会的发展、进步,将会使更多的少年儿童健康成长,这是人类社会的美好愿望。“春天像健壮的青年,有铁一般的胳膊和腰脚,领着我们上前去”是“比喻”和“拟人”兼用,将春天比作“有铁一般的胳膊和腰脚”的“青年”,有理想,有勇气,有作为,敢担当。春天这个“健壮的青年”,“领着我们上前去。”在此,作者纵情赞美春天。并迸一步揭示出:春天有着不可遏制的创造力和无限美好的希望。因此,应当踏着矫健的春天步伐,去创造更加美好幸福的新生活。

三个比喻句组成了“排比”修辞格。作者用三个形象化的比喻,渐次排比,讴歌春天,使作品气势迭起,也使整幅春景图更加丰润。作者还要以此印证:春天是新鲜、美丽、欢快、具有强大生命力的。作品以这三个比喻句收束全文,言简意赅,节奏明快,生动活泼,表现力极强。
纵观全篇,《春》鲜明地表现出田园牧歌式的清新格调和欢快情绪。它是一曲赞歌,唱出了春的美妙旋律;它是一首热情的诗,抒发了对春的企盼和眷恋;它是一幅优美的图画,展示出春的气息与魅力。

1.4 名家点评

现代散文家朱自清的白话散文对“五四”以后的散文作家产生过一定的影响。朱自清的散文可以说是诗的变体,具有诗的艺术特征。其中,《春》更是诗意盎然,以明快婉转的诗化语言、善于运用侧面烘托的诗歌表现手法、情景交融的诗化意境谱写了一曲春之赞歌。 ——殷玉香

1.5 作者简介

朱自清(1898 年 11 月 22 日—1948 年 8 月 12 日),原名朱自华,字佩弦,号秋实。原籍浙江绍兴,出生于江苏省东海县。现代散文家、诗人、学者、民主战士 [7] 。散文有《匆匆》、《春》、《你我》、《绿》、《背影》、《荷塘月色》《伦敦杂记》等,著有诗集《雪朝》,诗文集《踪迹》,文艺论著《诗言志辨》、《论雅俗共赏》等。

2. 测试列表与图文混排:《数据链路层》

  • 数据链路层
  • 帧:数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元,头部表名数据发送者、接受者、数据类型
  • MAC 地址:每个设备具有的硬件地址。数据链路层负责 MAC 地址,在网卡的 ROM 中
  • 链路:是从一个结点到相邻节点的一段物理链路,数据链路则在链路的基础上增加了一些必要的硬件(如网络适配器)和软件(如协议的实现)
  • 无论在多么复杂的网络中,从逻辑意义上讲,真正的数据传输通道就是数据链路层中所定义的数据链路
  • 数据链路层的三个基本问题是:封装成帧透明传输差错检测(可能还有物理地址寻址、流量控制、重发等)
  • 循环冗余检验 CRC 是一种检错方法,而帧检验序列 FCS 是添加在数据后面的冗余码
  • 数据链路层使用的信道主要有两种:点对点信道广播信道,对应的两种的常见的协议为:点对点协议 PPP以太网协议
  • 以太网协议点对点协议 PPP 均是数据链路层协议,区别在于以太网被设计用于广播网络,ppp 协议用于点对点网络,看帧格式就能明显看出区别来,以太网帧中有目标 Mac 地址,用于在多路信道确认目标端机器。而点对点协议中就没有目标 mac,点对点链路两端的主机事先就已经知道链路那头是哪个 ip 了
  • 以太网采用的无连接、不可靠的工作方式,对发送的数据帧不进行编号,也不要求对方发回确认,目的站收到有差错帧就把它丢掉,其他什么也不做

2.1 封装成帧

  • 其实就是在帧的数据部分添加帧首部和帧尾部,帧首尾用于帧定界,而数据部分才是传递给上层的数据(即 IP 数据报)
  • MTU:Maximum Transfer Uint,帧的数据部分的最大长度,即 IP 数据报长度
  • 我们将帧首部记作 SOH,帧尾部记作 EOT,用于帧定界
  • 帧的逻辑结构如下图所示,SOH、EOT 的具体内容则和具体协议有关,例如 PPP 协议和以太网协议有不同的帧格式
    帧结构图

2.2 透明传输

  • 由于我们使用了帧定界符 SOH,EOT,但数据部分也有可能包含定界符,从而导致帧定界出错,因此需要实现透明传输
  • 数据部分如果出现了 SOH,EOT,我们在其之前添加一个转义字符 ESC,这样就能将其识别为数据而不是帧定界符,如果数据部分也包含 ESC,则可以在 ESC 之前再添加一个 ESC 透明传输
  • 透明传输的方法主要有字节填充和零比特填充两种

2.3 差错处理

  • 差错产生的原因:数据信号从发送端发送到物理线路时,由于物理线路存在噪声,因此数据信号经过物理线路的噪声,到达接收端时,已经是数据+噪声的叠加,因此差错无法避免
    差错
  • 由于存在噪声,可能出现比特错误、帧丢失、帧重复和帧失序等错误
  • 差错控制主要两种策略:检错和纠错
    • 检测码:发送冗余信息让接收端用于检错
    • 纠错码:发送足够的冗余信息让接收端能发现并自动纠错
  • 由于纠错码实现比较复杂,检测码虽然不能纠错,但是足够简单,能够检测出差错,配合重传机制即可。所以广泛采用检测码
  • 循环冗余检验(CRC)即为最常见的检错方式

2.4 循环冗余检验 CRC

  • 为了保证数据传输的可靠性,CRC 是数据链路层广泛使用的一种检错技术
  • 其概念性运算如图所示
    CRC校验概念
  • CRC 校验举例如图所示
    CRC校验举例

2.5 点对点协议 PPP

  • 点对点协议 PPP 是数据链路层的一种协议,用于点对点信道,它的特点是:简单,只检测差错而不去纠正差错,不使用序号,也不进行流量控制,可同时支持多种网络层协议
  • 简单:接收方每接收一个帧,就进行 CRC 检验,检验正确,就收下,否则就丢弃,它是不可靠传输,所以这就是简单的原因
  • 用户计算机和 ISP 进行通信时使用 PPP 协议,例如 PPPoE 为宽带上网的主机使用的链路层协议(连着一条线,不用寻址)
  • PPP 协议帧格式如下
    PPP帧格式
  • F:即帧定界标志,规定为 0x7E,即 01111110
  • A:下一个目的地的 MAC 地址,由于点对点无需 MAC 地址,因此固定位 FF,没什么用
  • C:控制字段,固定位 03,没什么用
  • 协议:2 字节,描述协议类型
    • 0x0021:PPP 帧的信息字段就是 IP 数据报
    • 0xC021:PPP 链路控制协议 LCP 的数据
    • 0x8021:网络层的控制数据
  • FCS:用于 CRC 校验
  • 透明传输 采用字节填充或者零比特填充(SONET/SDH 链路时),异步传输时使用字节填充,同步传输时使用零比特填充

2.6 广播信道、以太网与局域网

2.6.1 概述与关系

  • 以太网:以太网是通信协议标准,该标准定义了在局域网(LAN)中采用的电缆类型和信号处理方法
  • 局域网:在较小范围内组建的网络,通过交换器什么的连接各个 PC 机,比如一个实验室,一栋楼,一个校园内,这都市局域网,拿网线将两台计算机连在一起,这也能算是局域网
  • 区别:以太网是一种局域网,而局域网却不一定是以太网,大多数局域网就是采用了以太网的这个标准
  • 在局域网中,就采用的是广播信道:就是一台 PC 机发送数据给另一台 PC 机,在同一个局域网中的计算机都能接收到该数据,这就像广播一样,所以这种就叫做广播信道

2.6.2 CSMA/CD 协议(半双工通信)

  • 局域网是用广播信道的方式去传送数据,那么就会遇到问题,如果在局域网内有两个 pc 机同时在其中传播数据呢?就会发生碰撞,使两个数据都失效,那么如何解决这个问题呢,使用 CSMA/CD 协议来解决这类问题
  • CSMA/CD 可简单描述为:多址接入、载波监听、碰撞检测
  • 多址接入:该协议为多址接入协议,许多站点以多址接入的方式链接在一根总线上,其实就是局域网中总线网这种形式
  • 载波监听:发送前监听,就是在发送数据前监听总线中是否有数据在传播,如果有就不发送。就是用电子技术检测总线上有没有其他计算机发送的数据信号
  • 碰撞检测:
    • 边发送边监听,在发送数据的中途也会监听总线中是否会有其它数据,当几个站同时在总线上发送数据时,总线上的信号电压摆动值将会增大(互相叠加)
    • 当一个站检测到的信号电压摆动值超过一定的门限值时,就认为总线上至少有两个站同时在发送数据,表明产生了碰撞。 所谓“碰撞”就是发生了冲突。因此“碰撞检测”也称为“冲突检测”
  • 检测到碰撞之后的处理 :
    • 在发生碰撞时,总线上传输的信号产生了严重的失真,无法从中恢复出有用的信息来
    • 每一个正在发送数据的站,一旦发现总线上出现了碰撞,就要立即停止发送,免得继续浪费网络资源,然后等待一段随机时间后再次发送

传播时延对载波监听的影响

  • 争用期:发生碰撞所需要的最迟时间,即 $2 \tau$
  • 10 Mbps 的以太网标准规定连接的最大长度为 2500 m,对应的争用期为 51.2μs,则在争用期内 10 Mbps 的以太网可发送 64 字节数据,因此我们发送帧至少要大于 64 字节,否则协议无法检测出是否发生碰撞
  • 对于 100 Mbps 以太网,由于速度增大,如果要保证发送帧的最短有效帧仍然为 64 秒,则我们需要将争用期变为原来的十分之一 5.12μs,因此百兆以太网允许最大连接长度要为 250 m,其争用期为 5.12μs,在争用期内最多发送 64 字节数据,因此仍然符合最短有效帧的定义
  • 由此可见,以太网的速率越快,以太网的有效距离就越短,对于 1000 Mbps 的以太网,要么放弃 CSMA/CD 协议改用其他协议,若仍要使用该协议,为了在争用期内检测出碰撞,就要再一次减小最大有效传输距离变为 25 m;若不想减小距离,则只能考虑将最短有效帧变为原来百兆以太网的 10 倍
  • 最短有效帧:64 字节,就是上面这样算的,发送了 64 个字节之后,肯定就不会发生碰撞,以太网规定了最短有效帧长为 64 字节,凡长度小于 64 字节的帧都是由于冲突而异常中止的无效帧
  • 根据 MAC 帧的格式,还有目的 MAC 地址 6 字节、源 MAC 地址 6 字节、类型 2 字节、FCS 4 字节,因此以太网数据部分最短 46 字节,最长即 MTU = 1500 字节

二进制指数类型退避算法

  • 确定基本退避时间,一般就是争用期 $2 \tau$
  • 确定参数 k = min(重传次数, 10)
  • 从整数集合 [0, 1, ..., 2^k-1] 中随机选取一个数,记作 r,重传所需要等待的时延就是 r 倍的基本退避时间($2 \tau \times r$)
  • 当重传 16 次还不能成功则丢弃该帧,并向高层汇报

2.6.3 以太网

  • 在局域网内部,以太网广播数据,并通过 MAC 地址确定目的方是否接受数据
  • MAC 地址:48 bit,6 字节,前 3 个字节是由管理机构给各个厂家分配的,也就是说如果有厂家想生产网卡这类需要 mac 地址的东西,必须先像管理机构申请前三位字节; 所以网卡上的前三个字节就代表着某个厂家,后三个字节就是由厂家自己来设定的
  • 每个网卡都拥有识别数据帧中 mac 地址的功能
  • 以太网定义的数据帧格式如图所示
    以太网帧
  • 目的地址、源地址即 MAC 地址,类型为数据包的类型,数据部分即为 IP 数据包,FCS 用于 CRC 校验
  • 开头的 8 个字节为前同步码(7 字节) + 帧定界(1 字节)
  • 前同步码是为了让接收方有反应时间,在接受 MAC 帧后,并不能马上识别出帧开始定界符,没有那么快的反应分辨出来,所以需要在帧定界前面加同步码,使接收方有反应的时间
  • 同步码都是 1010101010101 这样的 bit,前 7 个字节的同步码跟最后一个字节中的前 6 个 bit 位相同,只有最后两位不同,如图
    前同步码

3. 测试笔记与代码混排

3.1 lt:连续子序列最大和,经典 dp

  • 定义状态 dp[i] 表示 a[i] 为结尾的子序列最大和
  • 则我们可以得到状态转移方程 $dp[i] = \max(dp[i-1] + a[i], a[i])$
  • 因而最终结果 $res = max(dp[i])\ for\ all\ dp[i]$
  • 实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int maxSubArray(int[] nums) {

// dp[i] : 以 a[i] 为结尾的最大连续子序列和
// res = max(dp[i])

int sum = nums[0],res = nums[0];

// dp[i] = max(dp[i-1]+nums[i], nums[i])
for (int i = 1; i < nums.length; ++i) {
sum = Math.max(sum + nums[i], nums[i]);
res = Math.max(sum, res);
}

return res;
}
}

3.2 工具类

  • 以自定义的 ClassUtil 工具类为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package com.demo.base.component.util;

import java.lang.reflect.Field;
import java.util.*;

/**
* 实体工具类,通过反射打印实体和生成实例
*/
public class EntityUtil {

// 不想随机生成值的列,比如 deleteTime, 外键等,因为这些列不能生成随机值,需要设置为 null
private static Set<String> EXCLUDE_FIELD = new HashSet<>();

static {
// EXCLUDE_FIELD.add("enums");
}

public static String toString(Object obj) {
if (obj == null) {
return "null object";
}

try {
// 首先获取对象的所有域,并确定他们的名称
Class<?> objClass = obj.getClass();
Field[] fields = objClass.getDeclaredFields();

if (objClass == String.class) {
return (String) obj;
}

// 添加类名和左括号
StringBuilder sb = new StringBuilder();
sb.append(objClass.getSimpleName()).append("{");
// 取出域,拼接起来
for (Field field : fields) {
String fieldName = field.getName();
field.setAccessible(true);
sb.append(fieldName).append("=").append(field.get(obj)).append(", ");
}
// 删除最后两个字符并打上右括号
sb.delete(sb.length() - 2, sb.length());
sb.append("}");
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static String toString(Collection<?> objs) {

if (objs == null) {
return "[null list]";
}

StringBuilder sb = new StringBuilder();
sb.append("the size is ").append(objs.size()).append(", data is [\n");

for (Object obj : objs) {
sb.append(toString(obj)).append("\n");
}
return sb.append("]").toString();
}

public static <K, V> String toString(Map<K, V> map) {

if (map == null) {
return "{null map}";
}

StringBuilder sb = new StringBuilder();
sb.append("the size is ").append(map.size()).append(", data is {\n");

for (Map.Entry<K, V> entry : map.entrySet()) {
sb.append("{ key = ").append(entry.getKey())
.append(", value = ").append(entry.getValue())
.append(" }\n");
}
return sb.append("}").toString();
}

/**
* 利用反射技术,输出对象的字符串,这样就不必为每个对象编写 toString 方法,方便开发
*
* @param obj 要打印的对象
*/
public static void printString(Object obj) {
System.out.println(toString(obj));
}

/**
* 利用反射技术,输出集合中每个对象的字符串,这样就不必为每个对象编写 toString 方法,方便开发
*
* @param objs 集合
*/
public static void printString(Collection<?> objs) {
System.out.println(toString(objs));
}

public static <K, V> void printString(Map<K, V> map) {
System.out.println(toString(map));
}


public static <T> T generateRandomOne(Class<T> clazz) {
int num = (int) (Math.random() * 100); // 随机数
return generateRandomOne(clazz, num);
}

/**
* 利用反射技术,随机生成对象实例,其中 Id 列不设置,String 类型的列设置:为列名+随机数,整型直接设置为随机数
*
* @param clazz 目标类型的 Class
* @param <T> 泛型 T,即目标对象类型
* @return 返回生成的实例
*/
public static <T> T generateRandomOne(Class<T> clazz, int num) {
try {
T obj = clazz.newInstance();

Field[] declaredFields = clazz.getDeclaredFields();

for (Field field : declaredFields) {

String fieldName = field.getName();
Class<?> fieldType = field.getType();

// serialVersionUID 列跳过生成值
if (field.getName().equals("serialVersionUID") || EXCLUDE_FIELD.contains(fieldName)) {
continue;
}

field.setAccessible(true);

// 其他列,若是 String 类型,则根据 列名+随机数 进行赋值
if (fieldType == String.class) {
// 字符串类型设置为:列名+随机数
field.set(obj, field.getName() + num);
} else if (fieldType == int.class || fieldType == Integer.class) {

if (fieldName.equals("status")) {
// 是 status,估计是 byte,取模 128
field.set(obj, ((int) num) % 128);
} else {
// 若是整型,直接赋值
field.set(obj, num);
}
} else if (fieldType == long.class || fieldType == Long.class) {
field.set(obj, (long) num);
} else if (fieldType == double.class || fieldType == Double.class) {
field.set(obj, (double) num);
} else if (fieldType == float.class || fieldType == Float.class) {
field.set(obj, (float) num);
} else if (fieldType == boolean.class || fieldType == Boolean.class) {
field.set(obj, num % 2 == 0);
} else if (field.getType() == Date.class) {
field.set(obj, new Date());
}
// 其他类型不设置,留空
}
// 返回生成的对象
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

4. 其他测试

4.1 测试数学公式

  • 行内公式:$res = \sum_{i=1}^{n} a[i]$
  • 测试多行公式
  • 一般来说,KeTex 能支持上述两种比较简单的公式渲染,但下面的这种较为复杂的环境则需要 MathJax 的支持
  • 线性代数提供了一种压缩、便捷的方式来描述线性方程(MathJax):

4.2 测试表格

4.2.1 简单表格

比较项 RESTful RPC
性能 略低 较高
灵活度
应用 微服务架构 SOA 架构

4.2.2 表头很窄但内容很多的表格

比较项 RESTful RPC
概念 REST 代表表现层状态转移(representational state transfer),由 Roy Fielding 在他的论文中提出。REST 用来描述客户端通过某种形式获取服务器的数据,这些数据资源的格式通常是 JSON 或 XML。同时,这些资源的表现或资源的集合是可以修改的,伴随着行为和关系可以通过多媒体来发现 RPC 代表远程过程调用(remote procedure call),其中最著名的便是由 Facebook 开发,现在由 Apache 维护的 Thrift(感谢 Facebook 为开源社区做出的伟大贡献),其包含多种语言的实现。由于我们痛恨 XML 的数据格式繁杂,所以大多数 RPC 协议都是基于 JSON
灵活度
应用 微服务架构 SOA 架构

4.2.3 表头很多的表格

比较项 数据列 1 数据列 2 数据列 3 数据列 4 数据列 5 数据列 6 数据列 7 数据列 8 数据列 9 数据列 10 数据列 11 数据列 12 数据列 13 数据列 14 数据列 15
记录 1 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值
记录 2 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值
记录 3 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值 数据值