前言
Synchronized是Java中的重量级锁,在我刚学Java多线程编程时,我只知道它的实现和monitor有关,但是synchronized和monitor的关系,以及monitor的本质究竟是什么,我并没有尝试理解,而是选择简单的略过。在最近的一段时间,由于实际的需要,我又把这个问题翻出来,Google了很多资料,整个实现的过程总算是弄懂了,为了以防遗忘,便整理成了这篇博客。
在本篇博客中,我将以class文件为突破口,试图解释Synchronized的实现原理。
从java代码的反汇编说起
很容易的想到,可以从程序的行为来了解synchronized的实现原理。但是在源代码层面,似乎看不出synchronized的实现原理。锁与不锁的区别,似乎仅仅只是有没有被synchronized修饰。不如把目光放到更加底层的汇编上,看看能不能找到突破口。javap是官方提供的*.class文件分解器,它能帮助我们获取*.class文件的汇编代码。具体用法可参考这里。 接下来我会使用javap命令对*.class文件进行反汇编。
编写文件Test.java:
public class Test {
private int i = 0;
public void addI_1(){
synchronized (this){
i++;
}
}
public synchronized void addI_2(){
i++;
}
}
生成class文件,并获取对Test.class反汇编的结果:
javac Test.java
javap -v Test.class
Classfile /Users/zhangkunwei/Desktop/Test.class
Last modified Jul 13, 2018; size 453 bytes
MD5 checksum ada74ec8231c64230d6ae133fee5dd16
Compiled from "Test.java"
... ...
public void addI_1();
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 i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
... ...
public synchronized void addI_2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
... ...
通过反汇编结果,我们可以看到:
- 进入被synchronized修饰的语句块时会执行monitorenter,离开时会执行monitorexit。
- 相较于被synchronized修饰的语句块,被synchronized修饰的方法中没有指令monitorenter和monitorexit,且flags中多了ACC_SYNCHRONIZED标志。
monitorenter和monitorexit指令是做什么的?同步语句块和同步方法的实现原理有何不同?遇事不决查文档,看看官方文档的解释。
monitorenter
Description
The objectref must be of type reference.Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread > that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as > > follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor > and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
Notes
- A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a synchronized statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine's method invocation and return instructions, as if monitorenter and monitorexit were used.
简单翻译一下:
指令monitorenter的操作的必须是一个对象的引用,且其类型为引用。每一个对象都会有一个monitor与之关联,当且仅当monitor被(其他(线程)对象)持有时,monitor会被锁上。其执行细节是,当一个线程尝试持有某个对象的monitor时:
- 如果该对象的monitor中的entry count==0,则将entry count置1,并令该线程为monitor的持有者。
- 如果该线程已经是该对象的monitor的持有者,那么重新进入monitor,并使得entry count自增一次。
- 如果其他线程已经持有该对象的monitor,则该线程将会被阻塞,直到monitor中的entry count==0,然后重新尝试持有。
注意:
monitorenter必须与一个以上monitorexit配合使用来实现Java中的同步语句块。而同步方法却不是这样的:同步方法不使用monitorenter和monitorexit来实现。当同步方法被调用时,Monitor介入;当同步方法return时,Monitor退出。这两个操作,都是被JVM隐式的handle的,就好像这两个指令被执行了一样。
monitorexit
Description
The objectref must be of type reference.
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
简单翻译一下:
指令monitorenter的操作的必须是一个对象的引用,且其类型为引用。并且:
- 执行monitorexit的线程必须是monitor的持有者。
- 执行monitorexit的线程让monitor的entry count自减一次。如果最后entry count==0,这个线程就不再是monitor的持有者,意味着其他被阻塞线程都能够尝试持有monitor
根据以上信息,上面的疑问得到了解释:
monitorenter和monitorexit是做什么的?
monitorenter能“锁住”对象。当一个线程获取monitor的锁时,其他请求访问共享内存空间的线程无法取得访问权而被阻塞;monitorexit能“解锁”对象,唤醒因没有取得共享内存空间访问权而被阻塞的线程。为什么一个monitorenter与多个monitorexit对应,是一对多,而不是一一对应?
一对多的原因,是为了保证:执行monitorenter指令,后面一定会有一个monitorexit指令被执行。上面的例子中,程序正常执行,在离开同步语句块时执行第一个monitorexit;Runtime期间程序抛出Exception或Error,而后执行第二个monitorexit以离开同步语句块。为什么同步语句块和同步方法的反汇编代码略有不同?
同步语句块是使用monitorenter和monitorexit实现的;而同步方法是JVM隐式处理的,效果与monitorenter和monitorexit一样。并且,同步方法的flags也不一样,多了一个ACC_SYNCHRONIZED标志,这个标志是告诉JVM:这个方法是一个同步方法,可以参考这里。
Monitor
在上一个部分,我们容易得出一个结论:synchronized的实现和monitor有关。monitor又是什么呢?从文档的描述可以看出,monitor类似于操作系统中的互斥量这个概念:不同对象对共享内存空间的访问是互斥的。在JVM(Hotspot)中,monitor是由ObjectMonitor实现,其主要的数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //指向当前monitor的持有者
_WaitSet = NULL; //持有monitor后,调用的wait()的线程集合
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //尝试持有monitor失败后被阻塞的线程集合
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
可以看出,我们可以
- 通过修改_owner来指明monitor锁的拥有者;
- 通过读取_EntryList来获取因获取锁失败而被阻塞的线程集合;
- 通过读取_WaitSet来获取在获得锁后主动放弃锁的线程集合。
到这里,synchronized的实现原理已经基本理清楚了,但是还有一个未解决的疑问:线程是怎么知道monitor的地址的?线程只有知道它的地址,才能够访问它,然后才能与以上的分析联系上。答案是monitor的地址在Java对象头中。
Java对象头
在Java中,每一个对象的组成成分中都有一个Java对象头。通过对象头,我们可以获取对象的相关信息。
这是Java对象头的数据结构(32位虚拟机下):
其中的Mark Word,它是一个可变的数据结构,即它的数据结构是依情况而定的。下面是在对应的锁状态下,Mark Word的数据结构(32位虚拟机下):
synchronized是一个重量级锁,所以对应图中的重量级锁状态。其中有一个字段是:指向重量级锁的指针,共占用25+4+1=30bit,它的内容就是这个对象的引用所关联的monitor的地址。
线程可以通过Java对象头中的Mark Word字段,来获取monitor的地址,以便获得锁。
回到最初的问题
synchronized的实现原理是什么?从上面的分析来看,答案已经显而易见了。当多个线程一起访问共享内存空间时,这些线程可以通过synchronized锁住对象的对象头中,根据Mark Word字段来访问该对象所关联的monitor,并尝试获取。当一个线程成功获取monitor后,其他与之竞争monitor持有权的线程将会被阻塞,并进入EntryList。当该线程操作完毕后,释放锁,因争用monitor失败而被阻塞的线程就会被唤醒,然后重复以上步骤。
写在最后
我发现其实大部分答案都可以从文档中得到,所以以后遇到问题还是要尝试从文档中找到答案。
本人水平有限,如果本文有错误,还望指正,谢谢~