JVM 对 Synchronized 关键字的实现是怎样的?
结论先行:synchronized 在字节码层面是两条指令——同步代码块编译为 monitorenter / monitorexit(编译器保证异常路径也有一次 monitorexit),同步方法则在方法访问标志里打上 ACC_SYNCHRONIZED。运行时真正干活的是对象监视器(monitor,HotSpot 里是 ObjectMonitor),它依赖对象头 Mark Word 与等待队列工作,并按「偏向锁 → 轻量级锁 → 重量级锁」逐级升级。
一、字节码证据
public class SyncDemo {
private final Object lock = new Object();
private int count;
public void block() { // 编译后是 monitorenter / monitorexit
synchronized (lock) {
count++;
}
}
public synchronized void method() { // 编译后方法标志带 ACC_SYNCHRONIZED
count++;
}
}
javap -c -v SyncDemo.class | sed -n '/void block/,/^$/p'
# 关键片段:
# aload_0
# getfield #2 // lock
# dup
# astore_1
# monitorenter
# ... 业务代码
# aload_1
# monitorexit
# ... 异常路径 handler 中还有一次 monitorexit
# athrow
javap -v 里 method() 的访问标志会显示 ACC_PUBLIC, ACC_SYNCHRONIZED,由 JVM 在方法调用时隐式加锁解锁。
二、对象头与锁状态
| 锁状态 | Mark Word 标志位 | 开销 | 适用场景 |
|---|---|---|---|
| 无锁 | 001 | 0 | 单线程访问 |
| 偏向锁 | 101 | 一次 CAS 记录线程 ID | 同一线程反复进入(JDK 15 起默认禁用) |
| 轻量级锁 | 00 | 栈帧锁记录 + CAS 自旋 | 多线程交替执行、竞争短 |
| 重量级锁 | 10 | 操作系统互斥量,线程阻塞挂起 | 竞争激烈、持锁时间长 |
竞争升级路径:
单线程进入 ──偏向锁(记 ThreadID)──→ 另一线程竞争
│ CAS 替换 Mark Word 失败
▼
轻量级锁(栈上 Lock Record + 自旋)
│ 自旋超阈值 / 竞争加剧
▼
重量级锁(ObjectMonitor + 等待队列)
升级不可逆(除偏向锁可撤销),因为重量级锁的实现成本最高、回退没有收益。
三、ObjectMonitor 的结构
| 字段 | 作用 |
|---|---|
_owner | 当前持有锁的线程 |
_recursions | 重入次数,对应可重入语义 |
_EntryList | 阻塞等待获取锁的线程队列 |
_WaitSet | 调用 wait() 后进入的等待集合 |
_cxq | 竞争激烈的线程先入此单向队列 |
wait() 会把线程放入 _WaitSet 并释放锁,notify() 把它移回 _EntryList 重新竞争;notifyAll() 则是全部移回。这也是为什么 wait / notify 必须在 synchronized 块中调用——没有 monitor 就没有这两个队列。
四、相关参数与观察
# JDK 8:偏向锁默认开启,启动后延迟 4 秒生效
java -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -jar app.jar
# JDK 15+:偏向锁被废弃,参数只告警不生效
java -XX:+UseBiasedLocking -jar app.jar
# OpenJDK 64-Bit Server VM warning: Option UseBiasedLocking was deprecated
# 打印锁的膨胀与撤销(需 debug 版 JVM 才完整)
java -XX:+PrintFlagsFinal -version | grep -i biased
五、常见追问
| 追问 | 回答要点 |
|---|---|
为什么 monitorexit 有两个 | 一条对应正常退出,一条在异常处理器里,保证抛异常也能释放锁 |
| 偏向锁为什么被废弃 | 撤销成本高(需 safepoint)、现代应用竞争普遍、收益被 G1/ZGC 与更快的 CAS 抵消 |
| 锁消除和锁粗化是什么 | JIT 优化的两种:前者删掉不可能竞争的锁(如局部 StringBuffer),后者把循环内的锁合并到循环外 |
synchronized 与 ReentrantLock 底层差异 | 前者是 JVM 内置 monitor,后者是 AQS 的 state + 队列,前者不可中断、无公平模式 |
| 重量级锁一定慢吗 | 不一定,竞争激烈时阻塞挂起比自旋空转更省 CPU;自旋只适合短临界区 |
小结
字节码层是 monitorenter / monitorexit 与 ACC_SYNCHRONIZED,运行期是 Mark Word 加 ObjectMonitor 的锁升级。把「指令 → 对象头 → 监视器 → 升级路径」四层讲清,这道题就答到位了。