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