Java synchronized 深入解析:从字节码指令到锁升级机制
在 Java 并发编程中,synchronized 是最基础也是最常用的同步工具。很多人知道怎么用,但对底层的实现原理却知之甚少。今天我们从字节码、JVM 对象头以及锁升级机制三个层面,把 synchronized 的底层逻辑彻底讲清楚。
1. 字节码层面:monitorenter 和 monitorexit
当我们使用 synchronized 关键字时,无论是修饰代码块还是方法,编译器都会在生成的字节码中插入对应的指令。
同步代码块
对于 synchronized(object) { ... } 这种形式,编译器会在同步块的前后分别生成 monitorenter 和 monitorexit 指令。
public void method() {
synchronized (obj) {
System.out.println("hello");
}
}
反编译后的字节码大致如下(简化展示):
0: aload_0 // 加载 this
1: getfield #2 // 获取 obj 引用
4: dup // 复制引用
5: astore_1 // 存入局部变量表
6: monitorenter // 尝试获取锁
7: getstatic #3 // 获取 System.out
...
15: aload_1 // 准备释放锁
16: monitorexit // 正常退出同步块,释放锁
17: goto 25
20: astore_2 // 捕获异常
21: aload_1 // 再次准备释放锁
22: monitorexit // 异常退出同步块,释放锁
23: aload_2
24: athrow
25: return
这里有个细节值得注意:你会看到有两个 monitorexit 指令。第一个用于正常流程退出,第二个隐藏在异常处理逻辑中。这是为了确保即使同步块内部抛出异常,锁也能被正确释放,避免死锁。
同步方法
对于 synchronized 修饰的方法,方法常量池中会设置 ACC_SYNCHRONIZED 标志。当调用该方法的指令(如 invokevirtual)执行时,虚拟机会检查这个标志。如果设置了,线程会先尝试获取锁(实例方法是 this,静态方法是类对象),再执行方法体。方法执行完毕,无论正常返回还是异常抛出,都会自动释放锁。
2. JVM 底层实现:对象头与 Monitor
monitorenter 和 monitorexit 指令背后的具体实现,是 JVM 的核心。其关键在于 Java 对象头 和 Monitor。
2.1 Java 对象头(Mark Word)
在 HotSpot 虚拟机中,每个 Java 对象在内存中的布局分为三部分:对象头、实例数据、对齐填充。其中,对象头 是理解锁的关键。
对象头包含两部分信息:
- Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等。它是实现锁的'主战场'。
- Klass Pointer:指向类元数据的指针。
为了在极小的空间内存储尽可能多的信息,Mark Word 被设计成一个非固定的动态数据结构。它会根据对象的状态复用自己的存储空间。在 32 位 JVM 下,Mark Word 的最后 2 位()标识了对象的锁状态,锁的升级过程就体现在这 2 位的变化上。


