系列文章:
- Java多线程复习与巩固(一)–线程基本使用
- Java多线程复习与巩固(二)–线程相关工具类的使用
- Java多线程复习与巩固(三)–线程同步
- Java多线程复习与巩固(四)–synchronized的实现
- Java多线程复习与巩固(五)–生产者消费者问题(第一部分)
- Java多线程复习与巩固(六)–线程池ThreadPoolExecutor详解
- Java多线程复习与巩固(七)–任务调度线程池ScheduledThreadPoolExecutor
- Java多线程复习与巩固(八)–原子性操作与原子变量
- Java多线程复习与巩固(九)–volatile关键字与CAS操作
- ThreadPoolExecutor最佳实践–如何选择线程数
- ThreadPoolExecutor最佳实践–如何选择队列
1、温故知新
在上一篇文章的例子中有一个Counter类:
1 | static class Counter { |
为了实现线程同步我们使用了synchronized
关键字,而synchronized
关键字有两种用法:
同步方法:
1
2
3
4
5
6static class Counter {
private int c = 0;
public synchronized void increment() { c++; }
public synchronized void decrement() { c--; }
public int value() { return c; }
}同步代码块:
1
2
3
4
5
6static class Counter {
private int c = 0;
public void increment() { synchronized(this){c++;} }
public void decrement() { synchronized(this){c--;} }
public int value() { return c; }
}
2、反编译代码
我们把上面三段代码反编译一下,并取出increment
和decrement
两个方法的反编译代码:
未加同步
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
public void increment();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field c:I
5: iconst_1
6: iadd
7: putfield #2 // Field c:I
10: return
LineNumberTable:
line 3: 0
public void decrement();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field c:I
5: iconst_1
6: isub
7: putfield #2 // Field c:I
10: return
LineNumberTable:
line 4: 0同步方法
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
29public synchronized void increment();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field c:I
5: iconst_1
6: iadd
7: putfield #2 // Field c:I
10: return
LineNumberTable:
line 3: 0
public synchronized void decrement();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field c:I
5: iconst_1
6: isub
7: putfield #2 // Field c:I
10: return
LineNumberTable:
line 4: 0同步代码块
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
public void increment();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field c:I
9: iconst_1
10: iadd
11: putfield #2 // Field c:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 3: 0
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class Counter, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void decrement();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field c:I
9: iconst_1
10: isub
11: putfield #2 // Field c:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 4: 0
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class Counter, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
通过看底层字节码,可以看出以下几点:
- 普通方法和
synchronized
方法在方法内部没有任何区别,仅仅是synchronized
方法比普通方法多了一个ACC_SYNCHRONIZED
标志位,该标志位表示访问该方法需要同步访问(synchronized access) synchronized
同步代码块中由于要加载this引用,多了很多指令,而关键的两个指令是monitorenter
,monitorexit
。
3、JVM规范中的Monitor
Oracle官网提供的JVM规范中对monitorenter
和monitorexit
两个指令有以下介绍:
monitorenter指令用于进入对象的monitor
监视器,该指令的操作数是栈中的对象引用(objectref),这个对象引用必须是引用类型,不能是基本类型。每个对象都关联着一个monitor
监视器,当且仅当monitor
监视器有线程所有者(owner)才会被锁住,线程通过执行monitorenter
指令尝试获取对象关联的monitor
监视器的所有权:
- 如果一个对象(objectref)关联的
monitor
监视器的entry
为0,该线程进入monitor
监视器并将它的entry
设置为1。该线程则是monitor
的所有者。 - 如果一个线程已经拥有对象(objectref)关联的
monitor
监视器,就让它再次进入该monitor
监视器,并将entry
加1(可重入锁)。 - 如果另一个线程已经拥有了对象(objectref)关联的
monitor
监视器,那么该线程将会阻塞,直到monitor
监视器的entry
为0时再次尝试获取所有权。
注意:
- 如果引用对象为null,
monitorenter
指令将会抛出空指针异常。 monitorenter
指令可以配合一个或多个monitorexit
指令来实现synchronized
代码块。虽然它们提供了锁定语义,但在synchronized
方法中并不会执行这两个指令,而是在调用synchronized
方法时monitor
进入,在方法返回时monitor
退出,这个将由JVM方法调用和返回指令隐式地处理。- Java的同步机制除了要实现
monitorenter
和monitorexit
这样的操作,还应该包括等待monitor
监视器(Object.wait),通知等待monitor
监视器的其他线程(Object.notify和Object.notifyAll)。JVM指令中不会提供这些操作的支持。 - JVM规范并没有规定如何实现
monitor
与对象的关联。
monitorexit用于退出对象的monitor
监视器:
- 执行
monitorexit
指令的线程必须是该monitor
监视器的所有者。执行该指令时,该线程会将monitor
的entry
减一。如果entry
进入次数的结果为0,该线程将退出monitor
不再是它的所有者。其他进入monitor
的阻塞线程可以尝试获取该monitor
监视器。
JVM规范文档说的非常清楚明白,synchronized
关键字是由monitor
监视器实现的。
4、Hotspot中的锁及其优化
Hotspot介绍可以参考Wiki
Hotspot偏向锁的介绍可参考这里
4.1、轻量级锁、重量级锁、偏向锁
在Java Hotspot中,每一个对象前面都有一个类指针和一个头字段。头字段中存储了一个哈希码(HashCode)以及一个标志位,该标志位用于标识对象的年龄(新生代,老年代等),同时它也被用来实现轻量锁。下面这张图展示了头字段的位置以及不同对象状态下的字段值。
图的右侧描述了标准的对象锁定过程。只要对象没被锁住,头字段的后两位将会置为01。当一个方法在对象上同步时,头字段和对象指针会被存储到当前线程栈帧的lock record
上。然后VM通过compare-and-swap
操作尝试将lock record
的指针存入对象头字段中。如果成功(对象头字段与预期的hash~age~01
相等),那么当前线程就获取了锁。由于lcok record
总是按字边界对齐,头字段最后两位为00并以此标记对象被锁定。
如果由于对象之前已经锁住导致compare-and-swap
操作失败,虚拟机首先会测试对象头字段是否指向当前线程的方法堆栈。这种情况下,说明当前线程已经拥有对象锁,然后能安全地继续接下来的执行逻辑。对于这种递归锁住一个对象的情况,lock record
会被初始化为0而非对象的头字段。除非两个不同的线程并发竞争同一个对象的锁,thin lock
才必须升级膨胀成重量级的monitor
去管理等待的线程。
thin lock
比升级的锁消耗更小,但它的性能也会受影响,因为每个compare-and-swap
操作在多处理器上必须原子性地执行,然而大多数对象只会被一个特定线程加解锁。
在Java6中,这个问题被所谓的免存储偏向锁技术解决了。由于大部分对象大多数情况最多由一个线程锁定,为了避免过多无谓的CAS操作,我们允许线程偏向某个线程,这也是”偏向锁”(Biased Lock
)这个名字的来由。
只有第一次获取锁时会执行一次compare-and-swap
将上锁线程的ID存入对象头字段中,这时我们称这个对象偏向这个线程。将来同一线程的加解锁操作都不再需要任何原子操作或头字段修改操作,执行栈中的lock record
也不会被初始化为0了,因为线程不会再去检查偏向锁的对象。
当线程在偏向另一个线程的对象上同步时(产生多线程竞争同一个锁的情况),偏向锁将会被撤销,使对象看上去是以常规方式锁定的一样。遍历拥有偏向锁线程的堆栈,关联的lock record
将会按照thin lock
的策略进行调整,并将lock record
的指针置入对象头字段。当访问对象的哈希码时,偏向锁也被撤销,因为哈希码位与线程ID共享。
明确设计为多个线程之间共享的对象不适用于偏向锁定,比如生产者/消费者共同操作的队列。因此,如果某个类的实例在过去经常发生偏向锁撤销,则会禁用偏向锁,这叫做**批量撤销(bulk revocation)**。如果在禁用了偏向锁的类实例上调用锁定代码,则它将执行标准的轻量级锁。新分配的类实例会被标记为不可偏置。
类似的机制称为**批量偏置(bulk rebiasing)**,它优化了类的对象被不同的线程加解锁但从不并发的情况。它会使类的所有实例的偏向锁暂时无效,而不是禁用偏置锁。类中的epoch
值用于指示偏向锁有效性的时间戳。在对象分配时将该值复制到对象头中。然后,批量偏置可以有效地实现为适当类中的epoch
的增加。下一次要锁定此类的实例时,代码会在对戏那个头字段中检测到不同的值,并将对象重新映射到当前线程。
4.2、Hotspot源码分析
oopDesc类是对象类的顶级基类:
Java8之后Method和Class等数据不再继承自oopDesc体系,而是被作为MetaspaceObj移入元数据区管理。
这里的oop不是面向对象编程,而是Ordinary Object Pointers
的缩写。
意思就是对象的托管指针。正常情况下它与机器原生指针大小相同。Java应用程序和GC子系统会仔细跟踪托管指针,以便可以回收垃圾对象。此过程还可能涉及垃圾回收过程中存活对象的重新定位(复制算法与整理算法)。
关于垃圾回收的内容可以参考这篇文章
oopDesc的定义如下:
1 | class oopDesc { |
每个对象头部都有一个markOop类型的4字节头字段。
这个头字段里就记录着前面说到的轻量级锁、重量级锁、偏向锁等信息。
接下来我们就看synchronized
整个同步过程是如何操作这个头字段的。
在Hotspot中synchonized
同步由ObjectSynchronizer
实现。
1 | class ObjectSynchronizer : AllStatic { |
fast_enter的实现如下:
1 | void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { |
slow_enter的实现如下:
1 | void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { |
inflate的实现如下:
1 | ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { |
升级成ObjectMonitor锁之后,会调用它的enter方法。
ObjectMonitor
就是JVM规范中定义的与对象关联的监视器对象。
在ObjectMonitor中有以下几个重要字段:
1 | class ObjectMonitor { |
重点就是它的enter方法:
1 | void ATTR ObjectMonitor::enter(TRAPS) { |
JVM代码比较复杂,分析必有不详尽之处。可以参考这篇文章深入考究。
参考链接:
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
https://blogs.oracle.com/dave/biased-locking-in-hotspot
http://www.spring4all.com/article/16805
https://www.oracle.com/technetwork/java/6-performance-137236.html#2.1.1
https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
https://wiki.openjdk.java.net/display/HotSpot/CompressedOops
https://www.baeldung.com/jvm-compressed-oops
https://docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html
https://www.javazhiyin.com/24370.html