JDK21中多线程竞争导致ObjectMonitor的CAS状态转移逻辑剖析
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
多线程竞争导致ObjectMonitor的CAS状态转移
LightweightSynchronizer 膨胀过程中,多线程竞争导致 ObjectMonitor CAS 状态转移的具体实现逻辑分析。
1. 锁膨胀并发竞争的核心逻辑与状态机演进
在 JDK 21 的 -XX:+UseLightweightLocking 机制下,多个线程可能同时发现同一个 Java 对象需要进行锁膨胀。常见的触发场景包括:
- LockStack 溢出:某个线程嵌套/递归加锁深度达到 8 次,再次加锁触发 Bounds Check 跳转慢速路径。
- 多线程并发抢占:线程 A 持有轻量锁,线程 B 尝试获取锁失败,进入慢速路径主动发起锁膨胀。
- 全局/显式操作:线程在无锁对象上调用
Object.wait()、System.identityHashCode()等必须依附ObjectMonitor的 API。
由于锁膨胀本身是一个包含堆外内存分配(ObjectMonitor)、属性初始化、Mark Word 原子更新等多步骤的过程,多线程同时对同一个对象进行 inflate 时,HotSpot 必须确保只有一个线程能够成功挂载其分配的 ObjectMonitor,而其他并发竞争线程必须能无缝退避并复用已膨胀的 ObjectMonitor。
Mark Word 的状态演变路径
[ Unlocked: 001 ] / [ Fast-Locked: 000 ]
│
│ 多线程并发调用 ObjectSynchronizer::inflate_impl
▼
┌─────────────────────────────────────────────────────────────┐
│ 线程 A / B / C 并发执行 om_allocate() 预分配 ObjectMonitor │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 执行 object->cas_set_mark(monitor_mark, mark) │
└──────────────┬───────────────┬──────────────┘
│ │
CAS 成功 (Winner) CAS 失败 (Loser)
│ │
▼ ▼
[ Inflated: 10 (ObjectMonitor*) ] 调用 om_release 回收 Monitor
│ 重新自旋,直接读取 Winner 的 Monitor
▼ ▼
ObjectMonitor 挂载完成 / 线程进入 monitor->enter()
2. C++ 源码级逐行与逻辑解构 (synchronizer.cpp)
多线程竞争锁膨胀的核心无锁(Lock-Free)决胜逻辑位于 src/hotspot/share/runtime/synchronizer.cpp 中的 ObjectSynchronizer::inflate_impl(在部分分支代码库中直接为 inflate)。
核心源码实现 (src/hotspot/share/runtime/synchronizer.cpp)
ObjectMonitor* ObjectSynchronizer::inflate_impl(Thread* current, oop object, const InflateCause cause) {
// 建立自旋重试循环,应对 CAS 失败或并发竞争状态
for (;;) {
const markWord mark = object->mark();
// =========================================================================
// 场景 1:分支 1 —— 对象已经被其他并发胜出的线程成功膨胀 (Inflated)
// =========================================================================
if (mark.has_monitor()) {
ObjectMonitor* monitor = mark.monitor();
assert(monitor != nullptr, "monitor should be valid");
// 竞争失败者或后到者在此直接返回已经挂载成功的 ObjectMonitor 实例
return monitor;
}
// =========================================================================
// 场景 2:分支 2 —— 对象正在被其他线程执行膨胀 (INFLATING 中中间态)
// =========================================================================
if (mark.is_being_inflated()) {
// 遇到中间过渡态,当前线程退避自旋 (SpinPause),等待胜出线程完成 Mark Word 设置
SpinPause();
continue;
}
// =========================================================================
// 场景 3:分支 3 —— 对象处于 Fast-Locked (000) 或 Unlocked (001) 状态 (竞争发起点)
// =========================================================================
if (mark.is_fast_locked() || mark.is_unlocked()) {
// Step 3.1: 预先从当前线程的 Thread-Local Monitor List 或全局 Pool 中分配一个干净的 Monitor
ObjectMonitor* monitor = om_allocate(current, object);
// Step 3.2: 根据当前 mark 的状态初始化 ObjectMonitor 的 header 字段
if (mark.is_fast_locked()) {
// 若当前处于轻量锁状态,保存 prototype header,防止原始 HashCode/GC 年龄丢失
monitor->set_header(markWord::prototype());
} else {
// 若处于 Unlocked 状态,直接备份当前的 Unlocked markWord
monitor->set_header(mark);
}
// Step 3.3: 构造目标 Inflated 状态的 markWord (将 Monitor 指针编码,低 2 位设为 10)
markWord monitor_mark = markWord::encode(monitor);
// Step 3.4: 关键物理 CAS 决胜点:尝试将对象的 Mark Word 从原始 mark 替换为 monitor_mark
markWord old_mark = object->cas_set_mark(monitor_mark, mark);
// -----------------------------------------------------------------------
// 场景 3.4.1:物理 CAS 胜出者 (Winner)
// -----------------------------------------------------------------------
if (old_mark == mark) {
// 成功将 Mark Word 的低位改写为 10,并指向 monitor 实例。
// 此后,其他所有并发线程通过 object->mark() 均会命中有 monitor 分支 (has_monitor == true)
return monitor;
}
// -----------------------------------------------------------------------
// 场景 3.4.2:物理 CAS 失败者 (Loser)
// -----------------------------------------------------------------------
// 物理 CAS 失败说明:
// 1. 其他线程抢先完成了膨胀 CAS (old_mark 变成了 Inflated)
// 2. 锁持有者线程刚好执行了 fast_unlock 修改了 Mark Word
// 3. 其他线程在该对象上计算了 Identity Hash Code
// 必须将预先分配但未使用的 ObjectMonitor 重新回收至 Monitor Pool,防止内存泄漏
om_release(current, monitor, false);
// 重置后继续自旋,下一次循环将直接读取最新 markWord 并做出正确决策
continue;
}
// ... 其他罕见状态的 Fallback 处理 ...
}
}
3. 多线程 CAS 竞争的 4 种典型决胜场景分析
场景一:多线程并发膨胀的 Winner 与 Loser 裁决
假设线程 T1(LockStack 溢出)与线程 T2(尝试抢占锁)同时对 Object-X 触发 inflate_impl:
线程 T1 (Inflator 1) 线程 T2 (Inflator 2)
│ │
├─► 调用 om_allocate() 拿到 M1 ├─► 调用 om_allocate() 拿到 M2
│ │
├─► cas_set_mark(M1, Mark_Old) ├─► cas_set_mark(M2, Mark_Old)
│ (CPU 总线/缓存锁裁决) │ (CPU 总线/缓存锁裁决)
│ │
├─── Winner (CAS 成功: old == Mark_Old) └─── Loser (CAS 失败: old != Mark_Old)
│ │
│ Mark Word 变为 Inflated(M1) │ 发现 Mark Word 已被改写
├─► 返回 M1 并执行 monitor->enter() ├─► 调用 om_release(M2) 归还内存
│ ├─► 进入下一次 for(;;) 循环
│ ├─► 命中 mark.has_monitor()
│ └─► 读取并返回 M1,复用 T1 的 Monitor!
- 硬件级原子性保障:在 x86_64 架构下,
object->cas_set_mark会展开为带lock cmpxchg前缀的物理汇编指令。缓存一致性协议(MESI)保证了针对同一对象内存地址的写操作会被精确串行化,绝不可能出现两个线程同时 CAS 成功的情况。
场景二:膨胀线程 (Inflator) 与锁释放线程 (Unlocker) 的 Racing 冲突
假设线程 T1 正在对 Object-Y 执行膨胀,而持有 Object-Y 轻量锁的线程 T2 恰好在此刻调用 fast_unlock 试图释放锁。
Race A:锁释放线程 T2 抢先 CAS 成功
- T2 执行
fast_unlock,用cmpxchg将 Mark Word 从Fast-Locked (000)还原为Unlocked (001)。 - T1 随后执行
cas_set_mark(M1, Fast-Locked),由于比较基准Fast-Locked已被修改为Unlocked,T1 的 CAS 宣告失败。 - T1 触发
om_release(M1)归还 Monitor,并在下一次自旋中读取到最新的Unlocked (001)Mark Word。 - T1 重新以
Unlocked对象的逻辑尝试膨胀,再次发起 CAS。
Race B:膨胀线程 T1 抢先 CAS 成功
- T1 抢先将 Mark Word 从
Fast-Locked (000)修改为Inflated (M1)。 - T2 执行
fast_unlock,尝试用 CAS 将 Mark Word 从Fast-Locked改回Unlocked。因为 Mark Word 已变成Inflated (10),T2 的 CAS 物理失败。 - T2 的 JIT 指令流发现 CAS 失败后,自动跳转至慢速路径
LightweightSynchronizer::exit。 - 在
LightweightSynchronizer::exit内部,T2 识别出 Mark Word 已经是Inflated状态,于是转为调用M1->exit(current)走重量级锁释放流程,递减ObjectMonitor内部的重入计数或唤醒cxq/EntryList中的等待线程。
// src/hotspot/share/runtime/lightweightSynchronizer.cpp
void LightweightSynchronizer::exit(oop object, current) {
markWord mark = object->mark();
if (mark.has_monitor()) {
// 锁释放时发现已被抢先膨胀,退化转交 ObjectMonitor 处理
ObjectMonitor* monitor = mark.monitor();
monitor->exit(current);
return;
}
...
}
场景三:膨胀完成后的 Owner 关系挂接 (inflate_and_enter)
在 JDK 21 的 -XX:+UseLightweightLocking 下,锁膨胀仅仅完成了 Mark Word 到 ObjectMonitor 的物理指针绑定,并不等同于当前线程已经成功获取了 ObjectMonitor 的持有权。
核心调用的二次绑定 (lightweightSynchronizer.cpp)
void LightweightSynchronizer::inflate_and_enter(Handle obj, BasicLock* lock, JavaThread* current) {
// Step 1: 执行膨胀(内部完成上述多线程 CAS 竞争裁决)
ObjectMonitor* monitor = ObjectSynchronizer::inflate(current, obj(), inflate_cause_vm_internal);
// Step 2: 显式调用 ObjectMonitor::enter() 竞争 Monitor 所有权
monitor->enter(current);
}
在 monitor->enter(current) 内部,会将 _owner 设为当前线程(或递减/递增重入计数)。如果原始锁是被其他线程持有的 Fast-Locked 状态,ObjectMonitor 内部会通过 try_set_owner_from 与自旋机制,安全地接管所有权或将当前线程放入 _cxq 队列挂起。
场景四:om_allocate 与 om_release 的无锁内存管理
为了防止高并发膨胀时频发调用 C-Heap malloc 导致内存碎片与分配瓶颈,ObjectMonitor 的分配采用三级缓存池设计:
- Thread-Local Free List (
_om_free_list):每个JavaThread私有的无锁 Monitor 链表。 - Global Free List (
g_free_list):全局无锁 Monitor 共享池,使用 CAS 进行无锁 Pop/Push 操作。 - C-Heap 分配:当前两级均无可用实例时,批量申请 C-Heap 内存。
在膨胀 CAS 失败(Loser)时,om_release 会立即将预分配的 Monitor 清空并放回当前线程的 _om_free_list 链表顶端,整个过程不涉及全局锁,性能极高。
4. 总结:JDK 21 锁膨胀状态转移表
| 初始 Mark Word 状态 | 并发事件 / 竞争操作 | CAS 状态转移结果 | 失败线程 (Loser) 的后续行为 |
|---|---|---|---|
Fast-Locked (000) | 多线程同时调用 inflate | Fast-Locked
→
\rightarrow
→ Inflated (10) | 归还预分配 Monitor,自旋复用已挂载的 Monitor |
Fast-Locked (000) | 线程 A inflate vs 线程 B fast_unlock | 若 A 胜:Fast-Locked
→
\rightarrow
→ Inflated (10) |
若 B 胜:Fast-Locked
→
\rightarrow
→ Unlocked (001) | A 胜:B 走 ObjectMonitor::exit
B 胜:A 以 Unlocked 状态重新尝试膨胀 |
| Unlocked (001) | 多线程同时调用 inflate | Unlocked
→
\rightarrow
→ Inflated (10) | 归还预分配 Monitor,自旋复用已挂载的 Monitor |
| Inflated (10) | 后续任何线程调用 inflate | 状态不变(直接命中 has_monitor()) | 无 CAS 操作,直接返回 mark.monitor() 指针 |

854

被折叠的 条评论
为什么被折叠?



