Skip to content

08.5 - 锁升级过程

定位: 锁升级是 HotSpot 针对"绝大多数锁无竞争"这一观察的极致优化——从偏向锁(零CAS)、到轻量级锁(CAS+自旋)、到重量级锁(OS互斥量),逐级降级代价、逐级提升公平性。 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 锁升级的完整路径、偏向锁撤销过程(安全点)、批量重偏向/撤销的触发条件、轻量锁膨胀为重量锁的时机、MarkWord 在每个阶段的变换

一、这是什么?为什么需要它?

锁升级的本质

WHY 需要锁升级 ─ 线程竞争锁的三种典型模式:
┌─────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│  模式 1: 无竞争 (90%+ 场景)                                              │
│  ┌───────────────────────────────────────────┐                         │
│  │ Thread-A:                                   │                         │
│  │   synchronized(obj) {   // 只有 A 自己使用   │                         │
│  │       // 操作 obj 的内部状态                 │                         │
│  │   }                                        │                         │
│  └───────────────────────────────────────────┘                         │
│  → 如果每次都用重量级锁: 线程 A 需要 park()/unpark() — 白费!              │
│  → 偏向锁: 第一次 CAS 标记 Thread ID, 后续检查即通过, 零同步开销         │
│                                                                         │
│  模式 2: 短时间竞争 (9% 场景)                                            │
│  ┌───────────────────────────────────────────┐                         │
│  │ Thread-A:  [synchronized(obj) { } ]       │                         │
│  │ Thread-B:  [synchronized(obj) { } ]       │ ← 刚好并发             │
│  │ 锁持有时间: 几十条指令 (~微秒)             │                         │
│  └───────────────────────────────────────────┘                         │
│  → 如果每次都用重量级锁: OS 上下文切换 > 锁持有时间! → 纯浪费               │
│  → 轻量级锁: CAS + 自旋等待 (微秒级) → 比线程切换快 10-100 倍            │
│                                                                         │
│  模式 3: 长时间或高竞争 (1% 场景)                                        │
│  ┌───────────────────────────────────────────┐                         │
│  │ Thread-A: [synchronized(obj) { //IO操作 }]│ ← 持有锁 10ms+          │
│  │ Thread-B:                                 │ ← 等待 10ms             │
│  │ Thread-C:  synchronized(obj) { }          │ ← 一直在自旋, 浪费 CPU  │
│  └───────────────────────────────────────────┘                         │
│  → 自旋不再高效 (自旋时间 > 锁持有时间) → 阻塞线程让出 CPU 更好           │
│  → 重量级锁: 线程阻塞 → OS 调度其他线程 → CPU 利用率提高                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

锁升级与 MarkWord 的关系

MarkWord 是整个锁升级的信息载体。每次升级的本质是 MarkWord 中锁状态位的变换

锁升级 = MarkWord 中存储的内容和状态位的改变

┌──────────────────────────────────────────────────────────────────────┐
│  锁状态      MarkWord 低 2 位     MarkWord 存储的内容                  │
│  ──────      ───────────────     ──────────────────────────           │
│  无锁               01            hashCode (如果已计算)                │
│  偏向锁             01            Thread ID (54 bit) + epoch          │
│  轻量级锁           00            Lock Record 指针 (栈上)              │
│  重量级锁           10            ObjectMonitor 指针 (堆中)            │
│  GC 标记            11            GC 线程标记信息                     │
└──────────────────────────────────────────────────────────────────────┘

核心观察:
- 无锁和偏向锁的 lock 位都是 01 → 通过 biased 位 (第 3 位) 区分
- 轻量/重量/GC 的 lock 位各不相同 → 直接可区分
- MarkWord 只有 64 位 — 每次升级都"切换"复用这些位

二、原理拆解

2.1 偏向锁 (Biased Locking)

何时进入偏向锁

场景: 线程首次获取锁

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  MarkWord 当前状态: 无锁 (biased=0, lock=01, hashCode=0)             │
│                                                                      │
│  synchronized(obj) 执行:                                             │
│  ① 检查 MarkWord.lock == 01 (无锁或偏向锁)                           │
│  ② 检查 MarkWord.biased == 0 (当前是无锁)                           │
│  ③ 如果没有计算过 hashCode (MarkWord.hashCode == 0):                │
│     → CAS 尝试将 Thread ID 写入 MarkWord                             │
│     → 期望: {unused:25 | hashCode:0 | unused:1 | age | 0 | 01}     │
│     → 写入: {threadID :54 | epoch:2 | age:4 | 1 | 01}              │
│     ├─ CAS 成功 → 获得偏向锁! 后续再进入只需检查 Thread ID = 零开销 │
│     └─ CAS 失败 → 有其他线程也写了 → 冲突 → 偏向锁撤销              │
│                                                                      │
│  如果 hashCode 已经计算过:                                           │
│     → MarkWord 中 hashCode 已经占用了 threadID 的 bit 位             │
│     → 直接跳过偏向锁, 进入轻量级锁                                   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

偏向锁的重入

同一个线程再次进入 synchronized(obj):

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  ① 检查 MarkWord.lock == 01                                       │
│  ② 检查 MarkWord.biased == 1 ← 说明是偏向锁                         │
│  ③ 检查 MarkWord 中的 Thread ID == 当前线程 ID:                      │
│     ├─ 匹配 → 什么都不做! 不需要 CAS, 不需要任何同步操作             │
│     │        直接进入同步块! 这就是偏向锁"零开销"的秘密               │
│     └─ 不匹配 → 锁被其他线程占用 → 启动偏向锁撤销流程               │
│                                                                      │
│  如果 epoch 不匹配 (类级别的批量重偏向导致):                           │
│     → 通过 CAS 重置 Thread ID 为当前线程                             │
│     → 相当于重新偏向到当前线程                                        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

偏向锁撤销 (Revocation)

这是最重要、最细节的知识点

场景: Thread B 试图获取 Thread A 持有的偏向锁

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  Thread B 执行 synchronized(obj):                                    │
│  ① 检查 MarkWord → biased=1, 但 Thread ID 不是自己                   │
│  ② 触发偏向锁撤销                                                   │
│                                                                      │
│  撤销流程 (必须在安全点 Safepoint 执行!):                            │
│  ┌──────────────────────────────────────────────────────────┐       │
│  │  ① JVM 请求全局安全点 (所有线程暂停在安全点)               │       │
│  │                                                          │       │
│  │  ② 检查 Thread A 的线程状态:                              │       │
│  │     ├─ A 已退出同步块 → Thread A 的栈帧中 Lock Record     │       │
│  │     │  已被清除 → 只需重置 MarkWord 为无锁状态             │       │
│  │     │                                                      │       │
│  │     └─ A 仍在同步块中 → A 的栈上仍有 Lock Record          │       │
│  │         → 将 MarkWord 升级为轻量级锁 (让 A 的 Lock        │       │
│  │           Record 成为轻量锁的锁记录)                       │       │
│  │                                                          │       │
│  │  ③ 安全点结束后, Thread B 进入轻量级锁 CAS 竞争          │       │
│  └──────────────────────────────────────────────────────────┘       │
│                                                                      │
│  WHY 必须在安全点执行?                                               │
│  ─────────────────────────                                          │
│  安全点保证:                                                          │
│  - 所有线程暂停 → 线程栈是稳定的 → 可以安全遍历 Lock Record          │
│  - 没有线程正在修改 MarkWord → CAS 安全                              │
│  - 可以修改线程的线程状态 (栈帧、Lock Record) 而不被干扰              │
│                                                                      │
│  安全点开销:                                                          │
│  - 所有线程暂停 → 如果暂停时间长 → 延迟/抖动                          │
│  - JDK 15 默认禁用偏向锁的原因: 撤销的安全点开销被频繁锁竞争放大      │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.2 轻量级锁 (Lightweight Locking)

何时进入轻量级锁

场景: 偏向锁撤销后, 两个线程 CAS 争抢

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  MarkWord 当前状态: 偏向锁已被撤销 → 无锁 (lock=01, biased=0)        │
│                                                                      │
│  轻量级锁加锁过程:                                                   │
│                                                                      │
│  ① 当前线程在当前栈帧中创建 Lock Record                              │
│     ┌──────────────────────────────────────┐                        │
│     │ 线程栈 (Thread Stack)                 │                        │
│     │ ┌────────────────────────────────┐   │                        │
│     │ │ 当前方法栈帧                    │   │                        │
│     │ │ ┌──────────────────────────┐   │   │                        │
│     │ │ │ Lock Record              │   │   │                        │
│     │ │ │ ├─  displaced_markword:  │   │   │ ← 保存原始 MarkWord   │
│     │ │ │ │    (原始 hashCode,      │   │   │   (解锁时 CAS 还原)   │
│     │ │ │ │     age 等)             │   │   │                        │
│     │ │ │ └─ owner: (指向 obj)     │   │   │                        │
│     │ │ └──────────────────────────┘   │   │                        │
│     │ └────────────────────────────────┘   │                        │
│     └──────────────────────────────────────┘                        │
│                                                                      │
│  ② CAS 尝试将 obj.MarkWord 从 {无锁} 改为 {指向 Lock Record 的指针}: │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │ CAS(期望: 无锁 MarkWord (hashCode,age,01)                │    │
│     │     写入: Lock Record 指针 (62 bits) + 00)                │    │
│     └──────────────────────────────────────────────────────────┘    │
│     ├─ CAS 成功 → 获得轻量级锁!                                     │
│     │   MarkWord.lock = 00                                          │
│     │   → 持有锁                                                   │
│     │                                                              │
│     └─ CAS 失败 → MarkWord 已经是 Lock Record 指针                  │
│         → 检查 Lock Record 是否在当前线程的栈帧中:                   │
│           ├─ 是 → 可重入! 增加 recusion count                       │
│           └─ 否 → 真正竞争 → 进入自旋等待                          │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

自适应自旋 (Adaptive Spinning)

WHY 自适应: 自旋的代价是"耗 CPU 在等待"。如果锁很快释放, 自旋值得;
如果锁持有时间很长, 自旋浪费 CPU。自适应自旋根据历史行为动态决策。

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  自适应自旋策略 (JDK 6+):                                            │
│                                                                      │
│  每次自旋尝试获取锁:                                                  │
│  ├─ 成功 → JVM 记录: 这次自旋成功了                                  │
│  │   → 下一次自旋次数增加 (乐观: 可能还会迅速释放)                     │
│  │                                                                   │
│  └─ 失败 (超出自旋次数) → JVM 记录: 这次自旋没等到                   │
│      → 下一次自旋次数减少 (悲观: 可能持有时间较长)                     │
│                                                                      │
│  动态调整:                                                            │
│  初始自旋次数: JVM 根据 CPU 核数估算 (一般为 100-1000 次)              │
│  成功 → 自旋次数增加 (上限 ~5000)                                     │
│  失败 → 自旋次数减少 (下限 ~10)                                       │
│                                                                      │
│  如果连续多次自旋失败: → 不再自旋, 直接膨胀为重量级锁                   │
│  如果连续多次自旋成功: → 自旋次数越来越多                              │
│                                                                      │
│  最佳场景: 锁持有时间 < 2 次上下文切换的开销 (~微秒级锁)              │
│  最差场景: 锁持有时间 > 10μs → 自旋浪费 CPU, 应切换到重量级锁        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.3 重量级锁 (Heavyweight Locking / Inflated)

何时膨胀

轻量级锁 → 重量级锁的触发条件(任一满足):
┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  条件 1: 自旋失败                                                   │
│  → 自适应自旋耗尽次数, 仍然没有获取到锁                             │
│  → 继续自旋会浪费 CPU → 膨胀为重量级锁让线程阻塞                    │
│                                                                      │
│  条件 2: 线程调用了 wait() / notify()                               │
│  → wait/notify 需要在 ObjectMonitor._WaitSet 上操作                  │
│  → 轻量级锁没有 WaitSet → 必须膨胀到重量级锁                        │
│  → 这是最直接的膨胀触发: wait() 调用 → 立即膨胀                     │
│                                                                      │
│  条件 3: 多个线程同时竞争 (>2)                                      │
│  → 轻量级锁的 CAS 竞争在多线程场景下成功率低                        │
│  → 并发越高, CAS 失败越频繁 → 膨胀                                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

膨胀过程 (Inflation)

轻量级锁 → 重量级锁的完整膨胀:

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  MarkWord 当前: Lock Record 指针 (lock=00)                           │
│                                                                      │
│  ① JVM 在堆上分配 ObjectMonitor 对象                                 │
│                                                                      │
│  ② 初始化 ObjectMonitor:                                            │
│     ┌──────────────────────────────────────────────────────┐        │
│     │ _owner       = 当前持有轻量级锁的线程                   │        │
│     │ _EntryList   = 竞争线程列表 (从栈上的 Lock Record 获取)│        │
│     │ _WaitSet     = 空 (还没 wait 调用)                    │        │
│     │ _recursions  = 轻量级锁的重入次数                      │        │
│     └──────────────────────────────────────────────────────┘        │
│                                                                      │
│  ③ CAS 将 MarkWord 从 Lock Record 指针 → ObjectMonitor 指针        │
│     期望: {ptr_to_LockRecord :62 | 00}                             │
│     写入: {ptr_to_ObjectMonitor:62 | 10}                           │
│                                                                      │
│  ④ 持有锁的线程继续执行 (_owner 不变, 仍然是该线程)                  │
│                                                                      │
│  ⑤ 竞争线程进入 _EntryList → park() → 线程阻塞                     │
│     → 不再消耗 CPU 自旋                                              │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.4 批量重偏向 (Bulk Rebias) 与批量撤销 (Bulk Revoke)

WHY 批量操作: 偏向锁撤销需要安全点暂停。如果大量线程交替获取同一类的锁,
每次撤销都触发安全点 → 性能灾难。JVM 引入类级别的批量优化。

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  类级别的 epoch 计数器:                                              │
│                                                                      │
│  每个类有一个 _biased_lock_epoch 计数器                              │
│  每个对象的 MarkWord 中也存有 epoch (2 bit)                          │
│                                                                      │
│  当某个类的偏向锁撤销次数达到阈值:                                    │
│                                                                      │
│  ┌────── 阈值 20 (BiasedLockingBulkRebiasThreshold) ──────┐         │
│  │                                                          │         │
│  │  触发: 批量重偏向 (Bulk Rebias)                          │         │
│  │                                                          │         │
│  │  行为:                                                    │         │
│  │  ① 增加该类的 epoch 计数器                               │         │
│  │  ② 当下一次该类的对象被偏向锁获取时, epcoh 不匹配       │         │
│  │     → 不需要安全点撤销! 直接 CAS 重偏向到新线程         │         │
│  │     (因为 epoch 变了, 旧的偏向锁自动失效)                │         │
│  │                                                          │         │
│  │  效果: 大量线程交替获取同一类对象的锁时, 不需要每次都    │         │
│  │        触发安全点撤销 → 性能大幅提升                     │         │
│  └──────────────────────────────────────────────────────────┘         │
│                                                                      │
│  ┌────── 阈值 40 (BiasedLockingBulkRevokeThreshold) ────┐           │
│  │                                                          │         │
│  │  触发: 批量撤销 (Bulk Revoke)                           │         │
│  │                                                          │         │
│  │  行为:                                                    │         │
│  │  ① 永久禁用该类的偏向锁                                   │         │
│  │  ② 该类的所有实例 → 直接走轻量级锁路径                     │         │
│  │  ③ 该类的所有线程: 撤销已持有的偏向锁                     │         │
│  │                                                          │         │
│  │  效果: 如果一个类被大量线程持续竞争, 偏向锁始终无法        │         │
│  │        保持 → 完全禁用偏向锁, 避免后续所有安全点撤销      │         │
│  └──────────────────────────────────────────────────────────┘         │
│                                                                      │
│  决策树:                                                              │
│                                                                      │
│  偏向锁撤销次数:                                                     │
│  │                                                                   │
│  ├─ < 20 次 → 每次单独安全点撤销                                    │
│  ├─ 20-39 次 → 批量重偏向 (epoch++, CAS 重偏向, 无需安全点)         │
│  └─ ≥ 40 次 → 永久禁用该类的偏向锁                                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

三、图解全景

3.1 MarkWord 在锁升级中的完整变换

┌─────────────────────────────────────────────────────────────────────────┐
│                 MarkWord 变换: 从无锁到重量锁的全过程                      │
│                                                                         │
│  初始: 无锁 (hashCode 未计算)                                           │
│  ┌──────────────────────────────┬──────┬──────┬──────┐                 │
│  │    unused:25  |  hash:0      │unused│ age  │001   │ ← lock=01, bias=0│
│  │                (未计算)       │  (1)  │ (4b) │      │                 │
│  └──────────────────────────────┴──────┴──────┴──────┘                 │
│                      │ Thread A 首次获取锁                                │
│                      ▼                                                  │
│  偏向锁: CAS 写入 Thread ID                                             │
│  ┌─────────────────────────────────┬──────┬──────┬──────┐              │
│  │   Thread ID (54 bits)           │epoch │ age  │101   │ ← lock=01     │
│  │   (Thread A 的线程 ID)          │ (2b) │ (4b) │      │   bias=1      │
│  └─────────────────────────────────┴──────┴──────┴──────┘              │
│                      │ Thread B 尝试获取 → 触发偏向锁撤销                 │
│                      ▼                                                  │
│  偏向锁撤销 → 回到无锁                                                  │
│  ┌─────────────────────────────────┬──────┬──────┬──────┐              │
│  │   重新恢复为无锁状态              │unused│ age  │001   │ ← 无锁     │
│  │   (Thread ID 被清除)            │      │      │      │              │
│  └─────────────────────────────────┴──────┴──────┴──────┘              │
│                      │ Thread A 和 Thread B CAS 争抢                     │
│                      ▼                                                  │
│  轻量级锁: CAS Lock Record 指针                                        │
│  ┌──────────────────────────────────────────────────┬────────┐         │
│  │  指向 Thread A 栈帧中的 Lock Record (62 bits)     │ 00     │         │
│  │  (Lock Record 中保存了原始的无锁 MarkWord)        │ lock   │         │
│  └──────────────────────────────────────────────────┴────────┘         │
│                      │ 自旋失败 → 膨胀                                   │
│                      ▼                                                  │
│  重量级锁: CAS ObjectMonitor 指针                                       │
│  ┌──────────────────────────────────────────────────┬────────┐         │
│  │  指向堆中的 ObjectMonitor (62 bits)               │ 10     │         │
│  │  (ObjectMonitor._owner = Thread A)               │ lock   │         │
│  └──────────────────────────────────────────────────┴────────┘         │
│                                                                         │
│  MarkWord 的变化本质: 同一块 64 位内存, 不同锁状态 -> 不同含义的位域    │
└─────────────────────────────────────────────────────────────────────────┘

3.2 锁升级状态机 (完全版本)

┌─────────────────────────────────────────────────────────────────────────┐
│                    HotSpot 锁升级完全状态机                                │
│                                                                         │
│                                            wait(), notify() 调用        │
│   ┌─────────────────────────────────────────────────────────────┐      │
│   │                                                              │      │
│   │  无锁 (001)                                                    │      │
│   │  biased=0, lock=01                                            │      │
│   │    │                                                          │      │
│   │    │ CAS(无锁, threadID+epoch+age+101) 成功                    │      │
│   │    ▼                                                          │      │
│   │  偏向锁 (101)  ←───── 批量重偏向 (epoch++)                     │      │
│   │  biased=1, lock=01                                            │      │
│   │    │                                                          │      │
│   │    │ 其他线程 CAS 失败 → 安全点撤销                            │      │
│   │    ▼                                                          │      │
│   │  无锁 (001) 或 轻量级锁 (000) ←--- 如果原持有者仍在同步块中    │      │
│   │  lock=01         lock=00                                       │      │
│   │    │                                                          │      │
│   │    │ CAS(无锁, LockRecordPtr+00) 成功                          │      │
│   │    ▼                                                          │      │
│   │  轻量级锁 (000)                                                │      │
│   │  lock=00, MarkWord指向栈中 Lock Record                        │      │
│   │    │                                                          │      │
│   │    │ 自适应自旋失败 / wait()调用 / 多线程高竞争                 │      │
│   │    ▼                                                          │      │
│   │  重量级锁 (010)  ←─── 一次膨胀,永久重量级                       │      │
│   │  lock=10, MarkWord指向堆中 ObjectMonitor                      │      │
│   │    │                                                          │      │
│   │    │ 解锁后 → ObjectMonitor 保留 (不降级)                      │      │
│   │    │ 再次获取 → 走重量级锁路径                                  │      │
│   │    │ 安全点可能触发批量降级 (减少 GC 遍历开销)                  │      │
│   │    ▼                                                          │      │
│   │  GC 标记 (011) ←── GC 时临时标记用                             │      │
│   │  lock=11, GC 线程使用                                          │      │
│   └──────────────────────────────────────────────────────────────┘      │
│                                                                         │
│  关键决策:                                                               │
│  → 重量级锁不解锁回到轻量/偏向 (单向升级)                                │
│  → 原因: ObjectMonitor 提供了 wait/notify 语义, 轻量/偏向没有            │
│  → 如果锁再次被同一线程使用, 重量级锁也已足够快                           │
│  → 降级只发生在安全点 GC 时, 为了减少 GC 遍历对象 Monitor 的开销          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.3 线程竞争时间线

场景: 三个线程逐一竞争同一把锁
┌─────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│  时间 ──────────────────────────────────────────────────────────────→    │
│                                                                          │
│  ┌─── 初始: 无锁 ───┐                                                  │
│  │  lock=01          │                                                  │
│  └───────────────────┘                                                  │
│        │ t1 获取                                                          │
│        ▼                                                                │
│  ┌─── 偏向锁 (t1) ───┐  ┌─── 偏向锁 (t1) ───┐  ┌─── 偏向锁 (t1) ───┐ │
│  │  没有竞争, t1      │──│ t1 再次进入:      │──│ t1 退出:          │ │
│  │  CAS 标记线程 ID   │  │ 检查 ID 匹配:     │  │ 不需要 MarkWord   │ │
│  │  零 CAS 开销       │  │ 零开销           │  │ 恢复 (因为偏向后  │ │
│  │                    │  │                   │  │ MarkWord 不变)    │ │
│  └────────────────────┘  └───────────────────┘  └────────────────────┘ │
│                                                                          │
│        │ t2 尝试获取 lock ← 新线程                                         │
│        ▼                                                                │
│  ┌──────────────────── Safepoint ────────────────────┐                  │
│  │ 偏向锁撤销! 检查 t1 是否还在同步块中                  │                  │
│  │ t1 已退出 → MarkWord 重置为无锁                    │                  │
│  │ Safepoint 结束                                    │                  │
│  └─────────────────────────────────────────────────────┘                  │
│        │ t2 CAS 争抢                                                      │
│        ▼                                                                │
│  ┌─── 轻量级锁 ───────┐                                               │
│  │  t2 CAS 写入        │  ┌─── t2 自旋 (t3 也在自旋) ───┐               │
│  │  Lock Record 指针   │──│  t2 和 t3 CAS 争抢           │               │
│  │  lock=00            │  │  自适应自旋: 历史数据决定      │               │
│  │  t2 持有锁           │  │  自旋次数 (10-5000 次)      │               │
│  └──────────────────────┘  └────────────────────────────┘               │
│                                                                          │
│        │ 自旋超时 → 膨胀                                                   │
│        ▼                                                                │
│  ┌─── 重量级锁 ───────┐                                               │
│  │  分配 ObjectMonitor │  ┌─── t3 阻塞 ────────────┐                    │
│  │  t2 继续持有 _owner  │──│  t3 进入 _EntryList    │                    │
│  │  t3 park() 阻塞     │  │  #3 t2 释放锁:          │                    │
│  │                     │  │  unpark(t3)             │                    │
│  └──────────────────────┘  └────────────────────────┘                    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

四、实战验证

验证 1: JOL 观察锁升级过程

java
import org.openjdk.jol.info.ClassLayout;

public class LockUpgradeDemo {
    static int dummy = 0;  // 防止 JIT 死代码消除
    public static void main(String[] args) throws Exception {
        Object obj = new Object();
        
        System.out.println("=== 1. 无锁 ===");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        // 输出示例: MarkWord = 0x0000000000000001 (低位 001)
        
        // 不计算 hashCode → 可以进入偏向锁
        System.out.println("=== 2. 偏向锁 (Thread A 获取) ===");
        // JDK 8 默认开启偏向锁, 但 JOL 中偏向锁显示延迟 4 秒
        // 这里用 Thread.sleep 等偏向锁激活
        Thread.sleep(4500);
        synchronized (obj) {
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            // MarkWord = ...thread_id...101 → 偏向锁!
        }
        
        System.out.println("=== 3. 轻量级锁 (Thread B 竞争) ===");
        // 需要另一个线程来争抢, 这里先计算 hashCode 阻止偏向
        // 或者用新对象:
        synchronized (obj) {
            // 当前线程持有 obj, 另一个线程通过竞争触发轻量锁
            Thread t2 = new Thread(() -> {
                System.out.println("Thread B 尝试获取...");
                synchronized (obj) {
                    System.out.println("Thread B 获取到锁");
                }
            });
            t2.start();
            Thread.sleep(200);
            t2.join();
        }
        // 在高竞争下观察轻量级锁的 MarkWord
        
        System.out.println("=== 4. 重量级锁 (调用 wait) ===");
        // wait() 强制膨胀到重量级锁
        Thread t3 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("Thread C 持有锁, 调用 wait");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                try {
                    obj.wait(500);  // wait → 必须膨胀!
                } catch (InterruptedException e) {}
            }
        });
        t3.start();
        t3.join();
    }
}

验证 2: 偏向锁撤销监控

bash
# JVM 参数: 观察偏向锁行为
-XX:+UseBiasedLocking              # 启用偏向锁 (JDK 8 默认)
-XX:BiasedLockingStartupDelay=0    # 取消 4 秒的偏向锁延迟
-XX:+PrintBiasedLockingStatistics  # 打印偏向锁统计 (需 fastdebug 版本)

# 更实用的: 使用 -XX:+PrintFlagsFinal 查看默认参数
java -XX:+PrintFlagsFinal -version | findstr Biased

# 预期输出:
#     intx BiasedLockingBulkRebiasThreshold   = 20
#     intx BiasedLockingBulkRevokeThreshold   = 40
#     intx BiasedLockingDecayTime             = 25000

验证 3: 批量重偏向 / 批量撤销

java
import java.util.ArrayList;
import java.util.List;

public class BulkRebiasDemo {
    static class LockClass {
        // 空类, 用于锁测试
    }
    
    public static void main(String[] args) throws Exception {
        // 延迟 0: 立即启用偏向锁
        // 测试前设置 JVM 参数:
        // -XX:BiasedLockingStartupDelay=0
        
        List<LockClass> locks = new ArrayList<>();
        
        // Phase 1: Thread A 偏向锁获取 (创建 50 个对象, 每个被 A 偏向)
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                locks.add(new LockClass());
            }
            for (LockClass lock : locks) {
                synchronized (lock) {
                    // 偏向锁: Thread A 持有
                }
            }
            System.out.println("Thread A: biased " + locks.size() + " locks");
        });
        t1.start();
        t1.join();
        
        // Phase 2: Thread B 逐个获取 → 触发偏向锁撤销
        // 前 19 次: 每次安全点撤销 (慢!)
        // 第 20 次: 触发批量重偏向 (epoch++, 快速重偏向)
        // 第 40 次: 触发批量撤销 (永久禁用该类的偏向锁)
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                LockClass lock = locks.get(i);
                synchronized (lock) {
                    // 前 19 次: 每次引发安全点偏向锁撤销
                    // 20-39 次: 批量重偏向 (只需要 CAS, 不需要安全点)
                    // 40+: 批量撤销 (该类偏向锁永久禁用)
                }
                if (i == 18) {
                    System.out.println("  -> 第 19 次撤销完成 (准备批量重偏向)");
                }
                if (i == 38) {
                    System.out.println("  -> 第 39 次撤销完成 (准备批量撤销)");
                }
            }
        });
        t2.start();
        t2.join();
        
        System.out.println("Done. LockClass 的偏向锁状态: " +
            (t2.isAlive() ? "alive" : "ended"));
    }
}

验证 4: 对比三种锁的性能

java
// 伪代码: 三种竞争模式下的 JMH 基准
// 1. 无竞争 (偏向锁)→ 最快 (~10ns)
// 2. 两个线程交替 (轻量锁) → 较快 (~100ns-1μs)
// 3. 高竞争 (重量级锁) → 最慢 (~10μs+, 有线程切换开销)

五、面试视角

追问答案要点
锁升级的过程?无锁→偏向锁(CAS 写入 Thread ID)→轻量级锁(CAS Lock Record 指针+自旋)→重量级锁(ObjectMonitor+线程阻塞)。升级不可逆(除安全点批量降级)。
偏向锁的撤销过程?(1) 另一个线程尝试获取;(2) JVM 进入全局安全点(暂停所有线程);(3) 检查原持有线程是否存活:已退出→重置 MarkWord 为无锁;仍在同步块→升级为轻量锁;(4) 安全点结束,新线程 CAS 竞争。
什么是批量重偏向?当某个类发生 20 次偏向锁撤销时触发。JVM 增加该类的 epoch 计数器,所有该类的对象中 epoch 不匹配 → 无需安全点,直接 CAS 重偏向新线程。优化大量线程交替使用同一类对象的场景。
什么是批量撤销?当某个类发生 40 次偏向锁撤销时触发。JVM 永久禁用该类的偏向锁,所有实例直接走轻量级锁路径。避免持续的安全点开销。
轻量级锁什么时候膨胀为重量级锁?(1) CAS 自旋耗尽(自适应自旋判断自旋不再有效);(2) 线程调用了 wait()/notify()(需要 ObjectMonitor 的 WaitSet);(3) 并发线程数量超过阈值(CAS 成功概率低)。
自适应自旋是什么?JVM 根据过去自旋成功/失败的历史动态调整自旋次数。成功→增加自旋次数(乐观锁快释放),失败→减少自旋次数。初期默认为几百次,上限几千次。避免 CPU 浪费在长时间等待的锁上。
偏向锁为什么在 JDK 15 默认禁用,JDK 21 移除?偏向锁撤销需要全局安全点暂停所有线程。在高竞争应用(如 Web 服务器)中,频繁的偏向锁撤销导致大量安全点暂停,反而降低了性能。维护偏向锁的代码复杂性和安全点开销不再值得。现代应用大多高并发,偏向锁收益减少。
轻量级锁和重量级锁的区别?轻量锁:CAS+自旋(用户态,无上下文切换),适合短持有时间。重量锁:OS 互斥量(用户态→内核态切换),线程阻塞不耗 CPU,适合长持有或高竞争。轻量锁无法处理 wait/notify。
锁升级是单向的吗?为什么?基本上是单向的(不能从重量锁回到轻量锁)。原因:重量锁的 ObjectMonitor 持有 wait/notify 状态(_WaitSet),这些语义轻量锁不支持。安全点 GC 时可能批量降级,但这是 GC 优化,不是正常锁行为。
synchronized 是可重入锁吗?如何实现?是。偏向锁:检查 Thread ID 匹配即可。轻量锁:检查 Lock Record 是否已在当前线程栈帧中。重量锁:ObjectMonitor._recursions 计数。

📚 相关链接

  • **synchronized的JVM实现** — ObjectMonitor 结构、monitorenter/monitorexit 字节码
  • **volatile的内存语义** — volatile 与锁升级的对比
  • **对象内存布局(MarkWord与Klass与实例数据)** — MarkWord 锁状态位详解(技术前提)
  • **锁消除与标量替换** — 逃逸分析如何消除同步
  • ← 返回 **JMM索引**

Knowledge4J — Java 知识库