> 日期:2026-10-16 模块:**JVM 底层 + 并发编程(对象头 / 锁优化)**
> 适用岗位:高级 Java 工程师(5 年+)
---
## 今日面试题
**【题目】**
`synchronized` 的锁升级过程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)在 HotSpot 中是如何实现的?请结合**对象头 Mark Word** 和**关键源码**说明每一步的触发条件与转换时机。
**追问(面试官必问):**
1. 为什么 JDK 15(JEP 374)要默认禁用偏向锁,直到 JDK 18 彻底移除?
2. 调用一次 `Object.hashCode()` 后,这个对象还能进入偏向锁吗?
3. `Atomic::cmpxchg` 在轻量级锁里做了什么?自旋失败多少次会膨胀?
4. 重量级锁的 `ObjectMonitor` 里 `cxq` 和 `EntryList` 为什么要分成两个队列?
---
## 考察点
- 是否真正理解 **Java 对象内存布局** 与 `Mark Word` 在不同锁状态下的位域复用
- 是否清楚 `monitorenter` / `monitorexit` 字节码背后的 **HotSpot 解释器 + C++ 运行时** 执行路径
- 能否区分「**偏向锁依赖构造无竞争**」「**轻量级锁依赖自旋**」「**重量级锁依赖内核挂起**」三层设计哲学
- 对 **JDK 版本演进与工程取舍** 的敏感度(JEP 374 / JDK-8256425),这是区分「背八股」与「读过源码 + 有实战」的关键分水岭
- 是否能延伸到实战:**锁消除、锁粗化、JFR 观测 monitor 争用、Vector/Hashtable 的现代化替代**
---
## 参考答案
### 一、核心结论
1. `synchronized` 的锁状态**不是提前设定的,而是在运行时按争用激烈程度逐步「膨胀」** 出来的:偏向锁 → 轻量级锁 → 重量级锁,**升级单向不可逆**(除全局安全点的批量重偏向/批量撤销等极少数场景)。
2. 三种锁状态的载体是同一个 `Mark Word`(64 位 JVM 下共 64 bit):**通过最后 2 bit 的 `lock` 标志 + 第 3 bit 的 `biased_lock` 标志来区分状态**,其余位被不同状态复用(线程 ID / 哈希码 / 分代年龄 / 锁记录指针 / Monitor 指针)。
3. **偏向锁的本质是把「CAS 获取锁」降级为「一次指针比较」**,代价是撤销时必须 Stop-The-World 到全局安全点。JDK 15 起默认禁用、JDK 18 移除,根本原因是:现代应用普遍「线程池 + 并发容器」,偏向锁的收益场景消失而撤销开销被放大。
---
### 二、深入原理
#### 2.1 前置:对象内存布局与 Mark Word
一个 Java 对象在堆中的结构:
```
┌─────────────────────────────┐
│ Mark Word (8 bytes) │ ← 锁状态的唯一载体,与 ObjectMonitor 指针互斥复用
├─────────────────────────────┤
│ Klass Pointer (4/8 bytes) │ ← 指向方法区的类元数据(开启压缩指针为 4 字节)
├─────────────────────────────┤
│ 实例数据 (Instance Data) │ ← 字段,按 long/double → int/float → short/char → byte 重排序
├─────────────────────────────┤
│ 对齐填充 (Padding) │ ← 保证整体是 8 字节的整数倍
└─────────────────────────────┘
```
**64 位 JVM 下 Mark Word 的五种形态**(这是本答案的核心表,面试时能默写出来基本就赢了):
| 锁状态 | bit 63–25 | bit 24–2 | bit 2 | bit 1 | bit 0 (lock) | biased_lock |
| --------- | --------------------------- | -------------------------- | ------ | ------------ | ------------ | ----------- |
| **无锁** | unused (25) | **identity_hashcode (31)** | unused | **分代年龄 (4)** | `01` | `0` |
| **偏向锁** | **Thread ID (54)** | **epoch (2)** | unused | 分代年龄 (4) | `01` | `1` |
| **轻量级锁** | **ptr_to_lock_record (62)** | — | — | — | `00` | — |
| **重量级锁** | **ptr_to_monitor (62)** | — | — | — | `10` | — |
| **GC 标记** | 空(不记录信息) | — | — | — | `11` | — |
> 关键洞察:**`identity_hashcode`(31 bit)与「偏向锁的 Thread ID」在同一批 bit 上,二者互斥** —— 这正是追问 2 的答案来源。
#### 2.2 第一步:偏向锁(Biased Locking)—— 把 CAS 变成一次比较
**触发条件**:对象从未被加锁,且**只有一个线程**反复进入同步块。
**加锁流程**:
```
线程 T1 第一次进入 synchronized(obj)
│
▼
读取 obj.mark 的 biased_lock 位 == 0 ?
│ 是(处于「可偏向 / 匿名偏向」状态)
▼
CAS 把 T1 的 Thread ID 写入 obj.mark,同时置 biased_lock = 1
│
▼
成功 → 持有偏向锁
此后 T1 再次进入:只需比较 mark 中的 Thread ID == 自己?
│ 是 → 直接进入,零原子指令、零 CAS
▼
(不需要任何同步操作,这就是「偏」——锁偏向了 T1)
```
**撤销(revoke)流程**——成本所在:
```
线程 T2 尝试获取偏向锁
│
▼
发现 mark 中的 Thread ID != T2
│
▼
【代价】向 VM 线程发起偏向撤销,需要等到「全局安全点 Safepoint」(STW)
│
├─ 检查 T1 是否存活?
│ 否 → 直接撤销为「无锁」,T2 可再偏向
└─ T1 存活但已退出同步块?
是 → 撤销为「无锁」,T2 可再偏向
否 → T1 仍在同步块内,【升级为轻量级锁】
```
批量优化:JVM 维护 `BiasedLockingBulkRebiasThreshold`(默认 20 次)与 `BiasedLockingBulkRevokeThreshold`(默认 40 次),命中后触发**批量重偏向**或**批量撤销**,把某个类的所有实例一次性处理,避免逐个 STW。批量撤销后再新建的对象**直接标记为不可偏向**。
**偏向锁的「先天缺陷」**(面试加分项):
| 缺陷 | 说明 |
| ----------------------- | ----------------------------------------------------------- |
| **启动延迟 4 秒** | `BiasedLockingStartupDelay=4000`,JVM 启动前 4 秒不启用,因为启动阶段本就无竞争 |
| **`hashCode()` 会杀死偏向锁** | 见下 2.5 |
| **撤销要 STW** | 高并发下撤销风暴 = 频繁安全点 = 严重的尾延迟抖动 |
| **与虚拟线程天然冲突** | Loom 场景下虚拟线程数量成千上万,「偏向了谁」意义极小,而 epoch/revoke 的状态机极其复杂 |
#### 2.3 第二步:轻量级锁(Lightweight Locking)—— 用 CAS + 自旋换内核态切换
**触发条件**:存在线程争用,但**临界区执行很快**(争用时间极短),不值得付出内核挂起的代价。
**加锁流程**:
```
1. 在当前线程的栈帧中分配一块 Lock Record(BasicObjectLock)
2. 把 obj.mark 的「displaced header」拷贝进 Lock Record
3. CAS: 若 obj.mark 仍等于拷贝前的值 → 把 obj.mark 更新为「指向该 Lock Record 的指针」
├─ CAS 成功 → 获得轻量级锁,lock 位 = 00
└─ CAS 失败 → 说明有竞争,进入【自适应自旋】
├─ 自旋中对手释放 → 抢到
└─ 自旋超阈值 → 膨胀为重量级锁
```
**自适应自旋**:自旋次数不是固定的(JDK 6 之后废弃了 `-XX:PreBlockSpin=10` 的固定值写法),而是由 JVM 依据「同一锁上历史自旋成功率」动态调整——上次在这把锁上自旋成功过,这次就允许自旋更久;反之直接跳过自旋去膨胀。
**解锁流程**:CAS 把 Lock Record 中的 displaced header 写回 obj.mark。若失败,说明已膨胀为重量级锁,走 `ObjectMonitor::exit`。
#### 2.4 第三步:重量级锁(Heavyweight Locking)—— 交给 ObjectMonitor
**触发条件**:自旋失败 / 竞争激烈 / 显式 `wait()`。
此时 mark word 的 lock 位变为 `10`,指向一个由 JVM(C++ 堆)分配的 `ObjectMonitor`:
```
ObjectMonitor
├─ _owner : 当前持有锁的线程(关键:指向 Thread 对象)
├─ _recursions : 重入计数
├─ _cxq : Contention Queue,新来的竞争者先 CAS 头插到这里(单向链表)
├─ _EntryList : 有资格参与竞争的等待队列
├─ _WaitSet : 调用 wait() 后进入的队列
└─ _count / _waiters : 计数器
```
**为什么 `cxq` 和 `EntryList` 要分开?**(追问 4)
- `cxq` 是**抢占式入口**,新线程只做一次 `CAS` 头插,**无锁、无阻塞**,保证了入队的高吞吐;
- `EntryList` 是**有序的待唤醒队列**,由 `ObjectMonitor::exit` 按策略(`Knob_MoveNotifyee`,默认 QMode=2)从 `cxq` 批量搬到 `EntryList`,再由 `_owner` 释放时唤醒队首;
- 分开是为了**解耦「入队」与「调度」**:入队不阻塞,调度不打乱正在自旋的线程,同时配合 `QMode` 参数可以做公平 / 非公平的策略切换。
线程在 `EntryList` 中通过 `park()` 挂起(进入内核 `ObjectMonitor::_EntryList` → `pthread_mutex` / `futex`),唤醒需从用户态陷入内核态,**这是「重」的代价**。
#### 2.5 追问 2 详解:为什么 `hashCode()` 之后不能进入偏向锁?
```java
Object obj = new Object();
obj.hashCode(); // ← 这一行之后,obj 永远无法进入偏向锁状态
synchronized (obj) { ... } // 直接走轻量级锁
```
**原因**:偏向锁状态下,`Mark Word` 的 bit 24–2 这 31 bit 被 **Thread ID 的存储复用**了。而 `Object.hashCode()` 默认实现(`hashCode=5`,即默认 `ObjectSynchronizer::FastHashCode`)要求把 31 bit 的哈希码**持久化写进 Mark Word**(否则同一个对象两次 `hashCode()` 返回值不一致,违反 Java 规范)。
二者抢同一块内存,HotSpot 的选择是:**只要 Mark Word 里已经存在 identity hashcode,就禁止该对象进入偏向状态**(`mark->has_no_hash()` 检查失败 → 直接 `slow_enter`)。
> 延伸:如果是 `-XX:hashCode=0`(Park-Miller RNG)等非默认策略,行为也不同;`System.identityHashCode()` 同理。这也是「`Object` 的哈希码是惰性计算的」这一结论的底层原因——**默认策略下,未调用过 `hashCode()` 的对象,其 Mark Word 里根本没有哈希码**。
---
### 三、关键源码分析
> 源码版本:HotSpot JDK 8(`hotspot/src/share/vm/runtime/`),这是面试最常被追问的版本基线。
#### 3.1 入口:`monitorenter` 字节码 → 解释器 → `ObjectSynchronizer`
```cpp
// hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
// 字节码 monitorenter 最终落到这里
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj()); // 取出要加锁的对象
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) { // ← JDK 15 起默认为 false
// 走偏向锁快速路径
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// UseBiasedLocking 关闭时,直接走轻量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
IRT_END
```
**逐段注释**:
- `elem` 是当前栈帧中预分配的 `BasicObjectLock`(即「Lock Record」),`elem->lock()` 就是 displaced header 的存放位置;
- 第 9 行 `if (UseBiasedLocking)` 是整个分水岭:**JDK 15 之前默认 `true`,之后默认 `false`** —— 这就意味着在 JDK 15+ 上,所有对象直接进入 `slow_enter`,锁状态只剩「轻量级 ↔ 重量级」两种。
#### 3.2 偏向锁快速路径:`fast_enter` + `revoke_and_rebias`
```cpp
// hotspot/src/share/vm/runtime/synchronizer.cpp
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 非安全点:尝试「撤销并重偏向」
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return; // ← 成功把锁偏向给自己(或已偏向自己),直接返回,零 CAS 加锁完成
}
assert(!attempt_rebias, "can not rebias toward VM thread");
} else {
// 已在安全点(例如 VM 线程调用):只能撤销,不能重偏向
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 偏向失败 / 被撤销 / T1 仍在同步块 → 降级到轻量级锁
slow_enter(obj, lock, THREAD);
}
```
**关键点**:`revoke_and_rebias` 的返回值有三种语义——
| 返回值 | 含义 | 后续动作 |
| --------------------------- | ------------------- | ---------------- |
| `BIAS_REVOKED_AND_REBIASED` | 撤销成功且已偏向当前线程 | 直接返回,加锁完成 |
| `BIAS_REVOKED` | 撤销成功但未能偏向(如刚刚批量撤销过) | 继续走 `slow_enter` |
| `NOT_REVOKED` | 无需撤销(本来就没偏向) | 继续走 `slow_enter` |
#### 3.3 轻量级锁:`slow_enter` —— CAS 的核心战场
```cpp
// hotspot/src/share/vm/runtime/synchronizer.cpp
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
if (mark->is_neutral()) {
// ---- 路径 A:无锁状态(lock=01, biased_lock=0)----
// 1) 把 Mark Word 备份到 Lock Record 的 displaced header
lock->set_displaced_header(mark);
// 2) CAS:把 Mark Word 改写成「指向 Lock Record 的指针」
// 注意这里是原子 CAS,而不是普通的 set_mark
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
TEVENT(slow_enter: release stacklock);
return; // ← CAS 成功,拿到轻量级锁
}
// CAS 失败:说明有竞争,落到函数末尾去膨胀
} else if (mark->has_locker() &&
THREAD->is_lock_owned((address)mark->locker())) {
// ---- 路径 B:重入 ----
// 已经是本线程持有的轻量级锁,Mark Word 指向本线程栈上的 Lock Record
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL); // 重入时 displaced header 置空,用作重入计数
return; // ← 重入成功,无需 CAS
}
// ---- 路径 C:竞争激烈,空气锁;解锁前先占位避免误判 ----
lock->set_displaced_header(markOopDesc::unused_mark());
// 膨胀为重量级锁,并进入 ObjectMonitor
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
```
**逐段注释**:
- **路径 A** 的 `cas_set_mark` 是真正意义上的 **`Atomic::cmpxchg` 汇编指令**(x86 上是 `lock cmpxchg`),它带 `lock` 前缀,会锁总线/缓存行——**这是轻量级锁唯一的原子操作开销**;
- **路径 B** 展示了轻量级锁如何用「displaced header 置 NULL」表达重入(不像偏向锁有 `_recursions`):因为同一个线程的同一把锁会对应多个 `BasicObjectLock`(每个栈帧一个),`displaced_header == NULL` 的 Lock Record 就是一个重入计数单位,解锁时见 NULL 就只弹栈不写回;
- **路径 C** 先写 `unused_mark()`(即 `markOopDesc::unused_mark()`,位模式 `11`)是防止在膨胀过程中其它线程误判当前 mark 状态,属典型的「先占位再加锁」防御性编程。
#### 3.4 重量级锁:`ObjectMonitor::enter` 与 `cxq` / `EntryList` 流转
```cpp
// hotspot/src/share/vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD;
// ---- 1) 无竞争快速路径:CAS 把 _owner 从 NULL 改成自己 ----
void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
if (cur == NULL) return;
// ---- 2) 重入:_owner 已经是自己 ----
if (cur == Self) {
_recursions++;
return;
}
// ---- 3) 当前持有者是自己(轻量级锁自旋期间已持有,但 _owner 尚未更新)----
if (Self->is_lock_owned((address)cur)) {
assert(_recursions == 0, "invariant");
_recursions = 1;
_owner = Self;
return;
}
// ---- 4) 真·竞争:走慢路径 ----
for (;;) {
if (TrySpin(Self) > 0) break; // 自适应自旋再抢救一次
// 加入 cxq / EntryList,并 park 挂起
EnterI(THREAD);
if (!ExitSuspendEquivalent(Self)) break;
if (Self->is_terminated()) Thread::current()->os::set_thread_state(...);
}
}
// enter() 的收尾:赋值 _owner 并统计重入
void ATTR ObjectMonitor::EnterI(TRAPS) {
Thread * Self = THREAD;
...
for (;;) {
// 挂起前再抢一次,避免「唤醒后立刻又被抢走」的惊群损耗
if (TryLock(Self) > 0) break;
if (TrySpin(Self) > 0) break;
// 关键:把当前线程包装成 ObjectWaiter,CAS 头插到 _cxq
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev = (ObjectWaiter *) 0xBAD;
node.TState = ObjectWaiter::TS_CXQ;
ObjectWaiter * nxt;
for (;;) {
node._next = nxt = _cxq;
if (Atomic::cmpxchg_ptr(&node, &_cxq, nxt) == nxt) break; // ← CAS 头插,无锁
if (TryLock(Self) > 0) { ... break; } // 插队失败就再试抢锁
}
// 挂起(真正进入内核态)
Self->_ParkEvent->park();
if (TryLock(Self) > 0) break;
...
}
}
```
**`exit` 侧的队列搬运(解释 `cxq` vs `EntryList`)**:
```cpp
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
...
// 释放锁(重入计数递减归零后)
// 从 cxq 批量「搬」到 EntryList —— 由 QMode 决定搬运顺序
if (ObjectMonitor::Knob_MoveNotifyee == 2) { // 默认 QMode = 2
ObjectWaiter * List = _cxq;
_cxq = NULL;
_EntryList = List; // 整体搬迁,顺序不变
}
// 唤醒 EntryList 队首
ObjectWaiter * w = _EntryList;
if (w != NULL) {
_EntryList = w->_next;
w->TState = ObjectWaiter::TS_RUN;
_owner = NULL;
OrderAccess::fence();
w->_event->unpark(); // ← 唤醒 = 内核调用
}
}
```
> `Knob_MoveNotifyee` / `QMode` 就是「公平 vs 非公平」的开关:**默认非公平**,允许新到达的线程在 `TryLock` 中插队;设为公平模式后严格按 `cxq → EntryList` 顺序唤醒,代价是吞吐下降。这与 `ReentrantLock` 的公平/非公平设计思路完全一致,面试时可作为横向对比的亮点。
---
### 四、追问 1 详解:JDK 15 为什么禁用偏向锁?
**JEP 374 给出的官方理由(三条)**:
| 理由 | 技术本质 | 面试如何表达 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| **收益场景已消失** | 偏向锁的经典受益者是 `Hashtable` / `Vector` / `StringBuffer` 这类**每次访问都加 synchronized 的老式集合**。现代应用改用 `HashMap`/`ArrayList`(单线程)或 `java.util.concurrent` 并发容器(多线程),这些容器内部要么无锁(CAS),要么用更细粒度的 `ReentrantLock`,**根本不产生无竞争 synchronized** | 「偏向锁是在给老代码打补丁」 |
| **现代应用反而更慢** | 线程池 + worker 线程模式下,同一把锁会在不同 worker 之间交替持有,**偏向锁频繁撤销** → 频繁 safepoint → 吞吐与尾延迟双双恶化。SPECjbb2015 就是按「偏向锁关闭更优」设计的 | 「线程池模式天然与偏向锁相克」 |
| **代码复杂度 / 维护成本** | 偏向锁侵入了 HotSpot 大量组件(`BiasedLocking`、`epoch`、`BulkRebias`、`BulkRevoke`、`safepoint` 协作),阻碍同步子系统演进;同时与 **Loom 虚拟线程** 的设计直接冲突 | 「为了解锁 Loom 的架构演进」 |
**版本时间线**:
| 版本 | 状态 |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JDK 5 / 6 | 引入,默认启用(延迟 4s 激活) |
| JDK 6+ | 增加批量重偏向、批量撤销 |
| JDK 9+ | 可显式 `-XX:-UseBiasedLocking` 关闭 |
| **JDK 15** | **JEP 374:默认禁用**,相关参数(`UseBiasedLocking`、`BiasedLockingStartupDelay`、`BiasedLockingBulkRebiasThreshold`、`BiasedLockingBulkRevokeThreshold`、`BiasedLockingDecayTime`、`UseOptoBiasInlining`)标记废弃但**仍可手动启用** |
| **JDK 18** | JDK-8256425:**实现代码彻底移除**,参数变为 obsolete |
| **JDK 19+** | 参数 **expired**,传 `-XX:+UseBiasedLocking` 会直接 `Unrecognized VM option` 启动失败 |
**实战建议**:
- **JDK 8/11 生产调优**:如果压测发现线程池 + 高频 synchronized 有 safepoint 抖动,优先显式加 `-XX:-UseBiasedLocking`,而不是调 `BiasedLockingStartupDelay`;
- **JDK 15+**:什么都不用做,只要保证**启动脚本里没有残留的 `-XX:+UseBiasedLocking` / `-XX:BiasedLockingStartupDelay`**(否则 JDK 19+ 直接起不来)。上线前用 `grep -rE "UseBiasedLocking|BiasedLocking"` 扫一遍部署脚本、Dockerfile、K8s manifest;
- **观测手段**:用 **JFR**(`jdk.JavaMonitorEnter` / `jdk.JavaMonitorWait` 事件)看 monitor 争用和持锁时长,比看 GC 日志更直接;
```bash
# 用 JFR 采样 30s,定位锁竞争热点
java -XX:StartFlightRecording=duration=30s,filename=lock.jfr -jar app.jar
# 或对运行中进程动态开启
jcmd <pid> JFR.start duration=30s filename=lock.jfr
```
---
### 五、一图总结(锁升级全景)
```
synchronized(obj)
│
┌───────────┴────────────┐
UseBiasedLocking=true UseBiasedLocking=false
(JDK 8/11 默认) (JDK 15+ 默认)
│ │
▼ │
┌──── 无锁 / 可偏向 (01, bl=0) ───┐ │
│ CAS 写入 Thread ID,bl=1 │ │
└────────────┬───────────────────┘ │
▼ │
【偏向锁】Thread ID == 本线程? ──是──→ 零开销进入
│ 否(T2 来抢) │
▼ │
需要 Safepoint(STW)撤销偏向 │
├─ T1 已退出/已死 → 回「无锁」,可重偏向
└─ T1 仍在块内 → 升级 ↓ │
│
┌──── 轻量级锁 (00) ←──────────────────┘
│ 栈帧 Lock Record ← 拷贝 displaced header
│ CAS: obj.mark 指向 Lock Record
│ ├─ 成功 → 持锁执行临界区
│ └─ 失败 → 自适应自旋
│ ├─ 自旋中抢到 → 持锁
│ └─ 自旋超时 → 膨胀 ↓
▼
┌──── 重量级锁 (10) ────────────────┐
│ mark 指向 ObjectMonitor │
│ 竞争失败线程 CAS 头插 _cxq │
│ exit 时 cxq → _EntryList → unpark │
│ 代价:用户态↔内核态切换 │
└───────────────────────────────────┘
```
---
### 六、面试话术模板(30 秒电梯版)
> 「`synchronized` 的锁升级本质是**用对象头 Mark Word 的最后 3 个 bit 做状态机**。只有一个线程反复进入时走偏向锁,靠比较 Thread ID 免掉 CAS;出现第二个线程就要撤销偏向、升级为轻量级锁,此时用 `cas_set_mark` 把 Mark Word 指向栈上 Lock Record,再靠自适应自旋博一把;自旋失败才膨胀成重量级锁,落到 `ObjectMonitor` 的 `cxq`/`EntryList` 里 `park` 挂起。
> 需要注意 **JDK 15 起偏向锁已默认禁用、JDK 18 代码移除**,官方理由是收益场景被现代并发容器和线程池抹平而撤销开销被放大,同时它阻挡了 Loom 虚拟线程的架构演进——所以现在只剩下「轻量级 ↔ 重量级」两种状态。实战里我会用 JFR 的 `JavaMonitorEnter` 事件而不是猜,来定位真正的锁热点。」
---
### 七、延伸追问预演(自测)
1. **锁消除**:`StringBuffer` 拼接局部变量为什么可能被 JIT 消除掉 `synchronized`?(逃逸分析 `-XX:+DoEscapeAnalysis` + `-XX:+EliminateLocks`,要求对象不逃逸)
2. **锁粗化**:为什么 `for (i<1000) synchronized(lock){...}` 会被 JIT 粗化成一次性加锁?
3. **`-XX:+UseHeavyMonitors`** 有什么用?(跳过偏向/轻量级,直接膨胀,用于排查锁 bug)
4. **`ReentrantLock` 对比**:为什么它没有偏向锁的概念?它的 `AQS` 队列与 `ObjectMonitor` 的 `cxq/EntryList` 有何异同?
5. **`ObjectMonitor` 在 JDK 15+ 有什么变化?**(JEP 143 起的轻量级锁优化尝试、以及后续对小对象锁的性能改进)
6. **`unpark()` 的「许可」机制**:为什么 `LockSupport.park()` 不会像 `wait()` 那样丢失唤醒?
---
> **明日预告**:Day 02 计划覆盖「**MySQL InnoDB 的 MVCC 与 RR 隔离级别下的幻读彻底辩清**」——重点讲 `ReadView`、`trx_id` 可见性算法、`next-key lock` 与快照读/当前读的差别。
本文深入解析HotSpot JVM中synchronized锁的三级升级机制:偏向锁、轻量级锁与重量级锁。核心围绕对象头Mark Word的位域复用,阐明各锁状态的触发条件、转换时机及底层实现,涵盖CAS操作、自适应自旋、ObjectMonitor结构、cxq与EntryList队列分工等关键技术点,并结合JDK 15禁用及JDK 18移除偏向锁的演进原因,强调现代高并发场景下锁优化的工程取舍。
1645

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



