Skip to content

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索引**

Knowledge4J — Java 知识库