08.4 - synchronized 的 JVM 实现
定位: synchronized 是 Java 最基础的同步原语——在 JVM 层面通过 monitorenter/monitorexit 指令 + ObjectMonitor 实现。锁升级路径(偏向→轻量→重量)是 HotSpot 对"绝大多数锁无竞争"这一观察的极致优化。 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: synchronized 的字节码表示、ObjectMonitor 结构、锁升级过程和条件、偏向锁撤销过程、MarkWord 状态变换
一、这是什么?为什么需要它?
synchronized 的定位
synchronized 的使用形式:
┌────────────────────────────────────────────────────────────────────┐
│ │
│ 形式 1: 同步方法 │
│ ┌────────────────────────────────────────────┐ │
│ │ public synchronized void method() { │ │
│ │ // 方法体 │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ → JVM 通过方法 access_flags 中的 ACC_SYNCHRONIZED 标志识别 │
│ → 隐式获取 this (实例方法) 或 Class 对象 (静态方法) 的锁 │
│ │
│ 形式 2: 同步块 │
│ ┌────────────────────────────────────────────┐ │
│ │ synchronized(lock) { │ │
│ │ // 同步代码块 │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ → JVM 通过 monitorenter + monitorexit 字节码指令操作 │
│ → 显式指定锁对象 │
│ │
│ 为什么需要锁: │
│ CPU 缓存的异步性 (Cache + Store Buffer) 导致非 volatile 变量在 │
│ 多线程间无可见性保证。synchronized 建立了 happens-before 关系: │
│ unlock hb lock → 锁范围内所有修改可见 │
└────────────────────────────────────────────────────────────────────┘锁优化的演进史
HotSpot 的锁实现不是一蹴而就的,它经历了一个持续的演进过程:
JDK 版本 锁实现 动机
───────── ────── ────
JDK 1.0-1.4 只有重量级锁 (ObjectMonitor) 早期 JVM, 没有优化
JDK 5 引入 JSR-133 JMM, 锁偏向 (默认关闭) Java 5 性能改进
JDK 6 偏向锁默认开启 + 轻量级锁 + 自适应自旋 锁竞争分析的里程碑
JDK 15 默认禁用偏向锁 (JEP 374) 偏向锁维护成本高
JDK 21 移除以弃用的偏向锁 (JEP 492) 不再维护偏向锁代码
核心观察 (WHY 锁升级):
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 绝大多数锁在运行时表现为: │
│ - 只被一个线程获取 (单线程场景: 如局部 Vector/StringBuffer) │
│ - 被持有时间极短 (微秒级: 方法内部的同步块) │
│ - 几乎没有竞争 │
│ │
│ 如果每次获取都使用 OS 互斥量 (重量级锁) → 用户态/内核态切换 → 代价高 │
│ │
│ HotSpot 的应对: 针对不同竞争程度提供不同的锁实现 │
│ 无竞争 → 偏向锁 (仅 CAS 标记线程 ID) │
│ 低竞争 → 轻量级锁 (CAS + 自旋) │
│ 高竞争 → 重量级锁 (OS 互斥量 + 等待队列) │
│ │
└─────────────────────────────────────────────────────────────────────┘二、原理拆解
2.1 同步方法的 ACC_SYNCHRONIZED
同步方法在 Class 文件中的表示:
ClassFile {
access_flags; // 包含 ACC_SYNCHRONIZED (0x0020)
methods: {
method_info {
access_flags: ACC_PUBLIC + ACC_SYNCHRONIZED // ← 标志在这里!
Code_attribute {
// 普通 bytecode, 没有 monitorenter/monitorexit
}
}
}
}
JVM 执行时:
1. 检测到 ACC_SYNCHRONIZED 标志
2. 实例方法 → 自动获取 this 的 monitor
静态方法 → 自动获取 Class 对象的 monitor
3. 执行方法体
4. 方法正常返回或异常返回 → 自动释放 monitor
javap 输出对比:
┌────────────────────────────────────────────┐
│ public synchronized void syncMethod(); │
│ descriptor: ()V │
│ flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED│
│ Code: │
│ 0: iconst_1 │
│ 1: ireturn │
│ │
│ // vs 同步块: │
│ public void syncBlock(); │
│ flags: (0x0001) ACC_PUBLIC │
│ Code: │
│ 0: aload_0 │
│ 1: dup │
│ 2: astore_1 │
│ 3: monitorenter ← 显式指令 │
│ 4: iconst_1 │
│ 5: aload_1 │
│ 6: monitorexit ← 显式指令 │
│ 7: goto 15 │
│ 10: astore_2 ← 异常处理器! │
│ 11: aload_1 │
│ 12: monitorexit ← 异常路径也有! │
│ 13: aload_2 │
│ 14: athrow │
│ 15: ireturn │
│ Exception table: │
│ from to target type │
│ 3 7 10 any ← 保证异常时释放 | │
└────────────────────────────────────────────┘关键知识点:
- ACC_SYNCHRONIZED 不需要额外的异常处理——JVM 保证方法退出时释放
- 同步块需要两个 monitorexit(正常路径 + 异常路径)——防止死锁
- 异常表确保了 monitorexit 一定会被执行(即使出现异常)
2.2 同步块的双 monitorexit 为什么必要
正常执行路径:
┌────────────────────────────────────────────┐
│ monitorenter │
│ try { │
│ // 同步代码块 │
│ } finally { │
│ monitorexit ← 第 1 个: 正常退出 │
│ } │
└────────────────────────────────────────────┘
异常执行路径:
┌────────────────────────────────────────────┐
│ monitorenter │
│ try { │
│ // 同步代码块 │
│ throw new RuntimeException(); │
│ } │
│ catch (any) { │
│ monitorexit ← 第 2 个: 异常退出 │
│ throw; ← 重新抛出异常 │
│ } │
└────────────────────────────────────────────┘
为什么两个都是必需的:
如果只有一个 monitorexit 在末尾:
→ 正常执行路径释放了锁
→ 但异常发生时跳过末尾的 monitorexit → 锁永远不会释放!
→ 其他线程等待该锁 → 死锁!2.3 ObjectMonitor —— 重量级锁的核心结构
ObjectMonitor 是 HotSpot 中实现重量级锁的 C++ 对象:
ObjectMonitor 核心字段 (HotSpot C++ 源码):
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ObjectMonitor { │
│ _owner; // 持有锁的线程 (NULL 表示未锁定) │
│ _EntryList; // 等待获取锁的线程队列 (BLOCKED 状态) │
│ _WaitSet; // 调用了 wait() 的线程队列 (WAITING 状态) │
│ _recursions; // 重入计数 (synchronized 是可重入锁) │
│ _count; // 锁计数器 │
│ _cxq; // Contention Queue — 竞争线程的快速入队链表 │
│ _Responsible; // 负责唤醒的线程 │
│ _SpinFreq; // 自旋频率统计 (自适应自旋使用) │
│ _SpinClock; // 自旋时钟 │
│ } │
│ │
│ MarkWord 指向 ObjectMonitor 的指针 (重量锁状态): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MarkWord (64 bit): ptr_to_ObjectMonitor (62 bits) | 10 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘ObjectMonitor 的工作流程:
线程尝试获取锁 (重量级锁路径):
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Thread 尝试 enter(ObjectMonitor): │
│ ① CAS 尝试设置 _owner = Thread (期望 _owner == NULL) │
│ ├─ 成功 → 获得锁, _recursions=1, return │
│ └─ 失败 → 继续 │
│ │
│ ② try spin (自适应自旋): │
│ ├─ 成功 → 获得锁, 返回 │
│ └─ 失败 → 继续 │
│ │
│ ③ 入队等待 (线程阻塞): │
│ a. 将 Thread 放入 _cxq (Contention Queue) │
│ b. 将线程状态设为 BLOCKED │
│ c. 调用 OS 的 park() → 线程挂起 (进入内核态) │
│ │
│ 线程释放锁 (exit(ObjectMonitor)): │
│ ① _recursions-- │
│ ├─ 如果 _recursions > 0 → 持有锁, return (可重入) │
│ └─ 如果 _recursions == 0 → 准备释放 │
│ │
│ ② _owner = NULL │
│ ③ 唤醒等待队列中的线程: │
│ a. 从 _cxq 或 _EntryList 取出一个线程 │
│ b. 调用 unpark() → 线程恢复执行 │
│ c. 新的线程竞争锁 → 新一轮 CAS 尝试 │
│ │
└─────────────────────────────────────────────────────────────────────────┘2.4 synchronized 的可重入性 (Reentrancy)
synchronized 是可重入锁 — 同一个线程可以多次获取同一把锁:
public class ReentrantDemo {
public synchronized void outer() {
System.out.println("outer");
inner(); // 同一个线程, 同一把锁 (this)
}
public synchronized void inner() {
System.out.println("inner");
// 可重入: 不会死锁!
// _recursions: 1 (outer) → 2 (inner) → 1 (inner返回) → 0 (outer返回)
}
}
实现机制:
┌──────────────────────────────────────────────────────────────┐
│ 偏向锁: 重入直接检查 Thread ID 是否匹配 → 匹配 = 已持有锁 │
│ │
│ 轻量级锁: 检查 Lock Record 是否已在当前线程的栈帧中 │
│ → 如果已在 → 设置递归计数, 不需 CAS │
│ │
│ 重量级锁: ObjectMonitor._recursions++ │
│ → _owner == Thread → 重入 │
│ → 不需要再次阻塞 │
└──────────────────────────────────────────────────────────────┘2.5 wait/notify 的实现
wait/notify 只能在 synchronized 块中使用——这不是"设计限制",而是 ObjectMonitor 的必然结果。
wait() 的流程 (重量级锁):
┌─────────────────────────────────────────────────────────────┐
│ Thread 持有锁 → 调用 wait() │
│ ① 检查: 当前线程是否持有 ObjectMonitor (否则抛 IllegalMonitorStateException)│
│ ② 释放锁: _recursions = 0, _owner = NULL │
│ ③ 入队: Thread 移入 _WaitSet (ObjectWaiter 链表) │
│ ④ 线程阻塞: park() → 线程进入 WAITING 状态 │
│ │
│ notify() 的流程: │
│ ① 检查: 当前线程是否持有 ObjectMonitor │
│ ② 从 _WaitSet 取出一个线程 (或特定线程) │
│ ③ 将该线程移入 _EntryList → 等待重新获取锁 │
│ │
│ notifyAll(): │
│ ① 将 _WaitSet 中所有线程移入 _EntryList │
└─────────────────────────────────────────────────────────────┘
WHY 只能在 synchronized 内调用:
→ wait() 需要释放 monitor → 需要知道当前线程持有哪个 monitor
→ 检查 _owner == Thread → 必须在 synchronized 块内
→ 否则 JVM 不知道要释放哪个锁三、图解全景
3.1 monitorenter/monitorexit 字节码与异常表
javap -v 输出:
┌────────────────────────────────────────────────────────────────────┐
│ │
│ public void syncBlock(java.lang.Object); │
│ 0: aload_1 // 加载 lock 对象到操作数栈 │
│ 1: dup // 复制引用 (为异常路径保留) │
│ 2: astore_2 // 保存到局部变量 2 (异常路径用) │
│ 3: monitorenter // === 获取锁 === │
│ 4: getstatic // System.out │
│ 7: ldc "locked" │
│ 9: invokevirtual // println │
│ 12: aload_2 // 加载之前保存的引用 │
│ 13: monitorexit // === 释放锁 (正常路径) === │
│ 14: goto 22 // 跳过异常处理器 │
│ 17: astore_3 // 异常处理器: 异常对象存到 local3 │
│ 18: aload_2 // 加载保存在 local2 的 lock 引用 │
│ 19: monitorexit // === 释放锁 (异常路径) === │
│ 20: aload_3 // 加载异常对象 │
│ 21: athrow // 重新抛出异常 │
│ 22: return │
│ │
│ Exception table: │
│ from to target type │
│ 3 14 17 any ← monitorenter 到正常 monitorexit │
│ 之间的任何异常都跳到 17 │
│ │
│ 关键设计: │
│ → 异常表 from=3 包含 monitorenter 指令, 但 JVM 规定: │
│ 如果异常发生在 monitorenter 执行之前 → 没获取到锁, 不需要释放 │
│ → 异常表实际保护的是进入同步块后的代码 │
│ → 即使同步块内抛出异常, monitorexit 也会执行 → 锁一定被释放 │
│ │
└────────────────────────────────────────────────────────────────────┘3.2 MarkWord 到 ObjectMonitor 的映射
MarkWord 锁状态位图 (64 位 JVM):
┌──────────────────────────────────────────────────────────────────────┐
│ │
│ 无锁 (01, biased=0): │
│ ┌──────────┬────────────────────┬────────┬──────┬──────┬──────┐ │
│ │ unused │ identity_hashcode │unused │ age │biased│ lock │ │
│ │ (25 bits)│ (31 bits) │(1 bit) │(4b) │(1b)=0│ (2b) │ │
│ └──────────┴────────────────────┴────────┴──────┴──────┴──────┘ │
│ │
│ 偏向锁 (01, biased=1): │
│ ┌─────────────────────────────────────┬──────┬──────┬──────┐ │
│ │ thread ID + epoch (54 bits) │ age │biased│ lock │ │
│ │ │ (4b) │(1b)=1│ (2b) │ │
│ └─────────────────────────────────────┴──────┴──────┴──────┘ │
│ │
│ 轻量级锁 (00): │
│ ┌────────────────────────────────────────────────┬────────┐ │
│ │ Lock Record 指针 (62 bits) │ 00 │ │
│ │ (指向线程栈帧中的 Lock Record) │ (2 bits)│ │
│ └────────────────────────────────────────────────┴────────┘ │
│ │
│ 重量级锁 (10): │
│ ┌────────────────────────────────────────────────┬────────┐ │
│ │ ObjectMonitor 指针 (62 bits) │ 10 │ │
│ │ (指向堆中的 ObjectMonitor 对象) │ (2 bits)│ │
│ └────────────────────────────────────────────────┴────────┘ │
│ │
│ GC 标记 (11): │
│ ┌────────────────────────────────────────────────┬────────┐ │
│ │ (GC 线程使用) │ 11 │ │
│ └────────────────────────────────────────────────┴────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘3.3 锁升级状态机总览
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ HotSpot 锁升级状态机 │
│ │
│ ┌──────┐ 第 1 次获取 ┌──────────┐ 其他线程 CAS 失败 ┌──────────┐│
│ │ 无锁 │ ───────────────→ │ 偏向锁 │ ─────────────────→ │ 轻量级锁 ││
│ │ │ CAS 线程 ID │ (biased) │ │ (stack ││
│ │lock=01│ ←─────────────── │ lock=01 │ │ lock) ││
│ │bias=0 │ 偏向锁撤销 │ bias=1 │ │ lock=00 ││
│ └──────┘ └──────────┘ └────┬─────┘│
│ ↑ │ │ │
│ │ │ 另一个线程竞争 │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ 偏向锁撤销 │ 自旋失败或有等待 │ │
│ │ │ (安全点执行) │ ←────────────────── │ │
│ │ └──────────────┘ 或调用 wait() │ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────────────────────────────────┐ │
│ │ │ 重量级锁 │ │
│ └───────────────────│ (inflated) │ │
│ 锁降级 (安全点) │ lock=10 │ │
│ │ ObjectMonitor │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 转移条件: │
│ ① 无锁→偏向: 首次获取锁, CAS 写入线程 ID │
│ ② 偏向→轻量: 另一个线程尝试获取 → 偏向锁撤销 → CAS 竞争 │
│ ③ 轻量→重量: CAS 自旋失败 / 调用 wait() / 超过自旋次数 │
│ ④ 重量→无锁: 安全点降级 (后续提到, 但极少用) │
│ │
└─────────────────────────────────────────────────────────────────────────┘四、实战验证
验证 1: JOL 查看 MarkWord 锁状态变换
java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class MarkWordLockDemo {
static int counter = 0; // 防止死代码消除
public static void main(String[] args) throws Exception {
Object lock = new Object();
System.out.println("=== 无锁状态 (hashCode 未计算) ===");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// MarkWord: 0x0000000000000001 → 无锁 (biased=0, lock=01)
// 注意: hashCode 还未生成 (全 0)
System.out.println("=== 计算 hashCode 后 (无锁, 但无法偏向) ===");
System.identityHashCode(lock);
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// MarkWord: ...hashCode... → 无锁, hashCode 已写入
// 已无法进入偏向锁 (hashCode 占了 thread ID 的位)
System.out.println("=== 轻量级锁 (单个线程 synchronized) ===");
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// MarkWord: ...ptr_to_LockRecord → 轻量锁 (lock=00)
}
System.out.println("=== 重量级锁 (通过竞争触发) ===");
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1: holding lock");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// MarkWord: ...ptr_to_ObjectMonitor → 重量锁 (lock=10)
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(50); } // 等 t1 先获取锁
synchronized (lock) {
counter++;
System.out.println("Thread 2: got lock");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}验证 2: javap 查看同步块的双 monitorexit
java
public class SyncBytecodeDemo {
private final Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
System.out.println("in synchronized block");
}
}
public static void main(String[] args) {
new SyncBytecodeDemo().syncBlock();
}
}bash
# 编译并反汇编
javac SyncBytecodeDemo.java
javap -v -p SyncBytecodeDemo.class
# 输出中搜索 monitorenter/monitorexit
# 应看到两个 monitorexit 和一个异常表条目验证 3: JMH 比较三种锁的性能
java
// 伪代码: 三种锁竞争程度的 JMH 基准
@Benchmark
public void noContention() {
// 单线程 synchronized → 偏向锁或轻量锁
synchronized (lock) {
counter++;
}
}
@Benchmark
public void lowContention2() {
// 两个线程交替执行 → 轻量锁
// 模拟: 伪共享, 但 locks 不同
}
// 性能排序 (无竞争): 偏向锁 < 轻量锁 < 重量锁 (越轻量越快)
// 性能排序 (高竞争): 重量锁 > 轻量锁 > 偏向锁 (越重量越稳)
// 这就是锁升级的意义: 无竞争时轻量化, 高竞争时稳下来五、面试视角
| 追问 | 答案要点 |
|---|---|
| synchronized 的底层实现? | 两种形式:同步方法通过 ACC_SYNCHRONIZED 标志隐式获取锁;同步块通过 monitorenter/monitorexit 指令显式获取。JVM 根据竞争程度选择偏向锁、轻量锁或重量锁。重量级锁底层是 ObjectMonitor(C++ 对象),包含 _owner、_EntryList、_WaitSet、_recursions 等字段。 |
| synchronized 是可重入锁吗? | 是的。偏向锁通过检查 Thread ID 匹配实现可重入;轻量锁通过检测 Lock Record 是否已在线程栈上实现;重量锁通过 ObjectMonitor._recursions 计数实现。同一个线程多次获取同一把锁不会死锁。 |
| wait/notify 为什么必须在 synchronized 块中调用? | wait() 需要释放 monitor 的锁(将线程移入 _WaitSet),notify() 需要将线程从 _WaitSet 移回 _EntryList。这些操作都依赖 ObjectMonitor,需要知道当前线程持有哪个 monitor。如果不在 synchronized 内,JVM 无法确定要操作哪个 monitor。 |
| 为什么同步块需要两个 monitorexit? | 一个用于正常退出路径(同步块执行完毕),一个用于异常退出路径(同步块内抛出异常)。异常表中的处理器保证即使发生异常也能释放锁,防止死锁。如果没有异常路径的 monitorexit,异常发生时锁永远不会被释放。 |
| 什么是锁升级?为什么需要它? | 绝大多数锁无竞争。如果每次都用重量级锁(OS 互斥量),无竞争时浪费性能。HotSpot 根据竞争程度动态调整:无竞争→偏向锁(仅 CAS 标记线程 ID),低竞争→轻量锁(CAS+自旋),高竞争→重量锁(线程阻塞)。 |
| synchronized 和 ReentrantLock 的区别? | synchronized 是 JVM 内置(C++ 实现),支持自动锁升级,异常时自动释放。ReentrantLock 是 Java API(Unsafe + CAS),提供更灵活的功能(超时、可中断、公平锁、Condition),但需要手动释放锁。JDK 6 之后二者性能相近。 |
| 锁降级存在吗? | 理论上重量级锁不降级(单向升级),但在安全点 (Safepoint) 时 JVM 可以执行"批量锁降级"——如果发现所有等待线程都已完成,可以将 ObjectMonitor 释放回无锁状态。这只是为了 GC 效率,不是常规锁状态。 |
| ObjectMonitor 的 _EntryList 和 _cxq 有什么区别? | _cxq (Contention Queue) 是竞争线程快速入队链表,无锁操作(CAS),新来的竞争线程先放这里。_EntryList 是等待获取锁的正式队列。锁释放时,JVM 从 _cxq 迁移线程到 _EntryList,然后唤醒一个。这种设计优化了高并发下的入队性能。 |
📚 相关链接
- **JMM与happens-before** — 监视器锁规则是 8 条 hb 规则之一
- **锁升级过程** — 锁升级的完整过程、MarkWord 变换细节
- **volatile的内存语义** — volatile 与 synchronized 的对比
- **对象内存布局(MarkWord与Klass与实例数据)** — MarkWord 锁状态位详解
- **锁消除与标量替换** — 锁消除基于逃逸分析移除 synchronized
- ← 返回 **JMM索引**