synchronized 是 Java 中最基础的同步工具,理解它的底层实现对于掌握并发编程至关重要。我们通常只关注语法层面的使用,但深入 JVM 内部,会发现它是一套精妙的状态管理机制。
字节码层面:monitorenter 与 monitorexit
当你编写 synchronized 代码时,编译器会在字节码中生成对应的指令。无论是修饰方法还是代码块,核心都围绕着一对指令展开。
对于同步代码块 synchronized(obj) { ... },编译器会在同步区域前后分别插入 monitorenter 和 monitorexit。我们可以用 javap -v 反编译查看:
public void method();
Code:
0: aload_0
1: getfield #2 // Field obj:Lcom/example/Obj;
4: dup
5: astore_1
6: monitorenter // 尝试获取锁
...
16: monitorexit // 正常退出释放锁
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 异常退出也释放锁
23: aload_2
24: athrow
25: return
这里有两个关键点值得注意:
- 双重 monitorexit:第一个用于正常流程结束,第二个隐藏在异常处理逻辑中(类似 finally)。这是为了确保即使同步块内抛出异常,锁也能被正确释放,避免死锁。
- 同步方法:如果是
synchronized修饰的方法,常量池中会标记ACC_SYNCHRONIZED。调用时虚拟机自动处理加锁解锁,无需显式指令,但原理一致。
JVM 底层:对象头与 Monitor
指令背后的执行者,是 JVM 的 Monitor(管程)和 对象头。在 HotSpot 中,每个对象都有对应的 Monitor,而锁的状态信息就存储在对象的 Mark Word 里。
对象头结构
对象头包含两部分:Klass Pointer(指向类元数据)和 Mark Word(存储运行时数据)。Mark Word 是非固定的动态数据结构,会根据对象状态复用空间。其中最后几位标识了锁状态,这也是锁升级的核心依据。
Monitor 的作用
Monitor 是 JVM 级别的互斥锁,由 C++ 实现。它维护了线程与锁的关系:
- Owner:当前持有锁的线程,同一时刻只能有一个。
- EntryList:等待进入同步块的线程队列(Blocked 状态)。
- WaitSet:调用了
wait()方法的线程队列(Waiting 状态)。
当线程执行 monitorenter 时,会检查 Owner。如果为空,直接获取;如果已被占用,则进入 EntryList 阻塞等待。这种机制保证了同一时刻只有一个线程能操作临界区。
锁的升级与优化
早期版本的 synchronized 直接依赖操作系统互斥锁(重量级),开销大。JDK 6 引入了锁升级机制,根据竞争情况动态调整锁的粒度,路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
偏向锁
适用于单线程反复访问的场景。第一次加锁时,将线程 ID 写入 Mark Word。后续该线程再次进入无需 CAS 操作,只需比对 ID。一旦有另一个线程竞争,偏向模式失效,升级为轻量级锁。
注:Java 15+ 已废弃偏向锁,默认禁用,但理解其原理仍有助于分析旧版本行为。
轻量级锁
适用于存在轻微竞争的场景。线程通过 CAS 将对象头复制到栈帧中的锁记录(Lock Record),并尝试替换对象头。成功则获得锁,失败则自旋等待。若自旋后仍无法获取,说明竞争激烈,将膨胀为重量级锁。


