从字节码看 synchronized 关键字是怎么工作的

昨天面试的时候被问到 Java 中的synchronized关键字是什么原理,虽然凭着记忆打出来是通过控制对象头的 Monitor 来实现,但是毕竟没吃透这个知识点,还是没啥底气。干脆,这次就从字节码上看看,用了synchronized关键字的方法,到底是怎么执行的。

示例代码

说起synchronized的最简单的使用场景,我马上就想起双检单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
private static volatile Test INSTANCE;

private Test() {
}

public static Test getInstance() {
if (INSTANCE == null) {
synchronized (Test.class) {
if (INSTANCE == null) {
INSTANCE = new Test();
}
}
}

return INSTANCE;
}

public void print() {
System.out.println("test");
}
}

反编译成字节码

Test类先编译了,然后用javap -c Test.class反编译,就能看到这个类的字节码了。

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
Compiled from "Test.java"
public class Test {
public static Test getInstance();
Code:
0: getstatic #7 // 把静态变量INSTANCE加载到栈
3: ifnonnull 37 // 如果值不是null,那么跳转到标签37
6: ldc #8
8: dup
9: astore_0
10: monitorenter // 进入synchronized块
11: getstatic #7 // 把静态变量INSTANCE加载到栈
14: ifnonnull 27 // 如果值不是null,那么跳转到标签27
17: new #8 // new一个Test对象
20: dup
21: invokespecial #13 // 执行构造函数
24: putstatic #7
27: aload_0
28: monitorexit // 退出synchronized块
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #7 // Field INSTANCE:LTest;
40: areturn
Exception table:
from to target type
11 29 32 any
32 35 32 any

public void print();
Code:
0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #20 // String test
5: invokevirtual #22 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

注意看10: monitorenter28: monitorexit这两条字节码,这就是synchronized关键字实际做了的事。

Java 对象头和 Monitor

要说明白monitorentermonitorexit实际干了点啥,那就得先整明白 Java 对象的对象头。

一个 Java 对象,在内存中的布局包括三块区域:对象头、实例数据、和对齐填充。

别的东西咱们先不看,只看对象头这部分。对象头的最后 2bit 就存储了锁的标志位。

至于 Monitor,Java 官方文档是这么描述的:

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a “monitor.”) Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object’s state and establishing happens-before relationships that are essential to visibility.

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock before accessing them, and then release the intrinsic lock when it’s done with them.

同步是围绕着一个名为 “内在锁” 或 “monitor 锁” 的机制构建的。(API 规范文档中,通常会称其为 “monitor”)
内在锁一方面保证了针对一个对象的专属访问权限,另一方面保证了对可见性很重要的 happens-before 原则。
每个对象都会有一个与其相关联的内在锁。按照约定,如果一个线程需要持续持有对一个对象的独家访问权限,那么这个线程必须先获得到这个对象的内在锁,然后在执行完毕后释放掉这个内在锁。

代码执行到monitorenter指令,说明开始进入synchronized代码块,这时候 JVM 会尝试获取这个对象的monitor所有权,即尝试加锁;而执行到monitorexit指令,就说明要么synchronized代码块执行完毕,要么代码执行的时候抛出了异常,这时候 JVM 就会释放这个对象的monitor所有权,即释放锁。

继续深入细节

上面说的也是云里雾里的,咱继续往深处挖,看看具体的实现。

Monitor这个东西,看 Java 源码找不到,得找虚拟机的 C++ 源码。比如我们常用的 HotSpot 虚拟机中,Monitor是由ObjectMonitor类实现的:

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
// 为了解释方便,仅抄录了相关的代码,并重排了位置
class ObjectMonitor {
public:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

private:
volatile intptr_t _count; // reference count to prevent reclaimation/deflation
// at stop-the-world time. See deflate_idle_monitors().
// _count is approximately |_WaitSet| + |_EntryList|

// 等待锁的线程会被封装成ObjectWaiter对象
protected:
void * volatile _owner; // 一个指针,指向当前拥有锁的线程
ObjectWaiter * volatile _WaitSet; // 一个队列,保存着waiting状态的线程
ObjectWaiter * volatile _EntryList ; // 一个队列,保存着因等待锁而被阻塞的线程
}

当多个线程同时访问一段synchronized代码时,会发生这些操作:

  • 线程首先会进入_EntryList,在该线程获取到对象的monitor之后,_owner会指向这个线程,然后_count计数器加一。
    • 如果得到monitor的这个线程调用了wait()方法,那么这个线程将会释放掉 monitor 的所有权,_owner变量变回 NULL,_count计数器也会减一,同时这个线程会进入_WaitSet等待被唤醒。
    • 如果这个线程执行完毕,那么它也将释放monitor,并复位_count的值,这样其他的线程也就可以获得monitor来加锁了。
  • 上一个线程释放掉monitor后,_EntryList中的线程就会开始争抢monitor,具体哪个线程能成功得到monitor是不确定的。

而正因为 Monitor 对象存在于每个 Java 对象头的mark word中,所以每个 Java 对象都可以用作锁。

参考文章

  • synchronized 与对象的 Monitor
  • Intrinsic Locks and Synchronization
  • 啃碎并发(七):深入分析 Synchronized 原理
  • objectMonitor.hpp - JetBrains/jdk8u_hotspot
  • Why do we need to call ‘monitorexit’ instruction twice when we use ‘synchronized’ keyword? - StackOverflow