偏向锁到轻量级锁源码剖析
- 前言
- 偏向锁到轻量级锁源码剖析
-
- 核心演进逻辑与状态机
- [1. 系统视角的演进内核:为什么转换不可轻视?](#1. 系统视角的演进内核:为什么转换不可轻视?)
- [2. 偏向锁转轻量级锁的全局执行时序](#2. 偏向锁转轻量级锁的全局执行时序)
- [3. OpenJDK 8源码深度解析与详尽注释](#3. OpenJDK 8源码深度解析与详尽注释)
-
- [3.1 核心分流器:`synchronizer.cpp` 中的入口检测](#3.1 核心分流器:
synchronizer.cpp中的入口检测) - [3.2 运行时协调器:`biasedLocking.cpp` 中的单线程尝试与 Safepoint 唤起](#3.2 运行时协调器:
biasedLocking.cpp中的单线程尝试与 Safepoint 唤起) - [3.3 核心手术台:`biasedLocking.cpp` 中安全点内的栈帧重写](#3.3 核心手术台:
biasedLocking.cpp中安全点内的栈帧重写)
- [3.1 核心分流器:`synchronizer.cpp` 中的入口检测](#3.1 核心分流器:
- [4. 栈帧级内存布局异动对比](#4. 栈帧级内存布局异动对比)
-
- [升级前(线程 A 持有偏向锁)](#升级前(线程 A 持有偏向锁))
- [升级后(VM 线程在安全点内重写后)](#升级后(VM 线程在安全点内重写后))
- [5. 请求获取锁的线程 B 的后续命运](#5. 请求获取锁的线程 B 的后续命运)
- 总结:
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正
偏向锁到轻量级锁源码剖析
在 JVM 性能调优和高并发设计中,从偏向锁(Biased Lock)升级为轻量级锁(Lightweight Lock)是整个锁膨胀机制中最具技术含量的核心环节之一。
与"无锁 -> 偏向锁"只需单端 CAS 写入不同,从偏向锁向轻量级锁的升级,实质上伴随着一个高代价的偏向锁撤销(Revocation)过程。它不仅需要检查当前持有锁的线程状态,往往还需要借助 全局安全点(Safepoint) 挂起整个虚拟机来修改目标线程的调用栈。
核心演进逻辑与状态机
在 OpenJDK 8中,当线程 B 尝试获取一个已经被线程 A 偏向的对象锁时,就会触发该对象的锁升级。JVM 首先会"撤销"偏向锁状态,然后根据原持有者 A 的当前状态,决定将其降级为无锁,还是直接就地升级为轻量级锁:
- 交替执行(非真正竞争): 如果线程 A 已经退出了同步块(不处于存活状态,或者虽然存活但已经释放了锁),JVM 会将对象头恢复为普通无锁状态(001)。随后,线程 B 通过正常的轻量级锁 CAS 逻辑获取锁,锁升级为轻量级锁(00)。
- 激进竞争(真正竞争): 如果线程 A 依然保持在同步块内 (正在持有锁执行业务代码),JVM 会直接在 A 的栈帧中构建轻量级锁所需的
BasicObjectLock(锁记录),并将对象头(Mark Word)直接指向 A 线程栈中的这个锁记录。此时,对于 A 而言,锁在不知不觉中就地升级为了轻量级锁(00)。
1. 系统视角的演进内核:为什么转换不可轻视?
从偏向锁(Biased Lock)向轻量级锁(Lightweight Lock)的升级,是 HotSpot 虚拟机同步子系统中最为繁重、代价高昂的路径之一。
偏向锁的核心设计是一种"单方占有"的乐观模型:它假设锁在绝大多数情况下仅由一个特定线程(Thread A)反复获取,因此通过将 Thread A 的其指针写入 Mark Word,后续进入临界区只需进行简单的位掩码核对,完全避免了原子指令。
然而,当另一个线程(Thread B)尝试获取该锁时,这种乐观假设被打破。系统面临一个根本性的跨线程内存协调难题:
- 对象头(Mark Word)当前正指向 Thread A。
- Thread A 可能正在另一个 CPU 核心上高频执行该同步块内的代码,或者已经退出了同步块但并未主动擦除对象头中的偏向标记(因为偏向锁不会主动释放)。
- Thread B 无法在不与 Thread A 协调的情况下,盲目修改可能正在被 Thread A 依赖的对象头。
为了确保内存可见性与执行正确性,JVM 必须撤销(Revoke)偏向锁。如果检测到 Thread A 依然维持着对该锁的持有状态,系统就必须在保障并发安全的前提下,将锁结构彻底重构为基于线程本地栈帧的 轻量级锁(00 状态) 。在 OpenJDK 8的经典架构中,这一平滑转换通常需要借助全局安全点(Safepoint),由 VM 线程挂起所有 Java 线程后进行底层"外科手术"式的指针重写。
2. 偏向锁转轻量级锁的全局执行时序
- 多线程交替/竞争检测: 线程 B 在
ObjectSynchronizer::fast_enter路径中发现对象处于偏向锁状态(101),且偏向线程指针指向线程 A(而非自己)。 - 发起撤销请求: 线程 B 调用
BiasedLocking::revoke_and_rebias。由于无法直接修改线程 A 的状态,且无法判定 A 是否存活或正在同步块内,线程 B 构造一个VM_RevokeBias操作并提交给 VM 线程。 - 到达全局安全点(Safepoint): VM 线程响应请求,挂起所有应用程序线程(STW)。此时,全系统的内存状态处于绝对静态,消除了数据竞争。
- VM 线程探查堆栈(Stack Walking): VM 线程通过指针遍历线程 A 的所有栈帧(Stack Frames),寻找与当前锁对象关联的
BasicObjectLock(锁记录空间)。 - 锁状态内存重构(核心升级点):
- 场景一:若线程 A 已经退出了同步块(或已消亡): VM 线程直接将对象头重置为普通的无锁状态(
001)。安全点结束后,线程 B 按照正常的轻量级锁路径,通过 CAS 压入自己的锁记录。 - 场景二:若线程 A 依然在同步块中: VM 线程代表线程 A 执行轻量级锁的构建工作。它将对象原本的无锁 Mark Word(Displaced Mark Word)写入线程 A 最高层栈帧的锁记录中,然后将对象头的 Mark Word 修改为指向线程 A 栈帧锁记录的指针,并将锁标志位置为
00。
- 安全点解除与滚入慢路径: 恢复执行后,线程 A 此时无缝切换为以轻量级锁模式继续运行。线程 B 恢复执行,重新尝试获取锁,此时由于对象头已被修改为由 A 持有的轻量级锁(
00),线程 B 的 CAS 必定失败,从而滚入ObjectSynchronizer::slow_enter路径,准备向重量级锁(ObjectMonitor)发起进一步膨胀。
3. OpenJDK 8源码深度解析与详尽注释
这一过程的核心源码分布在三个文件:
hotspot/src/share/vm/runtime/synchronizer.cpp(同步器入口与分流)hotspot/src/share/vm/runtime/biasedLocking.cpp(安全点撤销与栈重写核心状态机)hotspot/src/share/vm/runtime/vmOperations.cpp(安全点任务封装,此处省略次要包装)
3.1 核心分流器:synchronizer.cpp 中的入口检测
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()) {
// 核心调用:尝试撤销并重新偏向。
// 对于多线程竞争场景,此函数内部会因为无法即时处理而向 VM 线程申请 Safepoint
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
// 如果返回 BIAS_REVOKED_AND_REBIASED,说明是匿名偏向成功或重偏向成功,直接返回
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
// 如果调用时不幸已经在安全点内,则直接调用安全点专用撤销函数
assert(SafepointSynchronize::is_at_safepoint(), "must be at safepoint");
BiasedLocking::revoke_at_safepoint(obj);
}
}
// --------- 升级/升级后落脚点 ---------
// 当上述偏向锁撤销逻辑执行完毕(例如在安全点内将偏向锁升级为了轻量级锁),
// 或者是对象本身就不支持偏向时,线程 B 将滚入此处的慢速路径。
slow_enter(obj, lock, THREAD);
}
3.2 运行时协调器:biasedLocking.cpp 中的单线程尝试与 Safepoint 唤起
cpp
// 文件物理路径: hotspot/src/share/vm/runtime/biasedLocking.cpp
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called at safepoint");
markOop mark = obj->mark();
// 检查对象是否为偏向锁模式 (低3位是否为 101)
if (mark->has_bias_pattern()) {
JavaThread* biased_locker = mark->biased_locker();
// 如果 biased_locker 指针不为主,说明该锁当前已被某个具体线程持有
if (biased_locker != NULL) {
// 场景 A: 锁虽然是偏向锁,但持有者就是当前线程自己
if (biased_locker == THREAD) {
// 属于偏向锁重入,汇编层未命中时进入此处,直接返回成功
return BIAS_REVOKED_AND_REBIASED;
}
// 场景 B: 核心竞争点!偏向持有人是线程 A,而当前请求获取锁的是线程 B
// 此时线程 B 必须强制撤销线程 A 的偏向状态,由于涉及跨线程修改对方的执行上下文明细,
// 线程 B 无法单方面完成,必须依赖 VM 线程投递一个安全点任务(VM_Operation)。
// 构造一个安全点撤销偏向的任务投递给底层 VMThread
VM_RevokeBias revoke_op(obj, THREAD);
VMThread::execute(&revoke_op); // 此处会触发 STW 挂起所有线程,直至 VM 线程处理完毕
// 安全点结束后,当前业务线程被唤醒,读取 VM 线程留下的处理状态
return revoke_op.result();
}
}
return BIAS_REVOKED;
}
3.3 核心手术台:biasedLocking.cpp 中安全点内的栈帧重写
当所有线程在 Safepoint 陷入静止后,VM 线程开始执行 BiasedLocking::revoke_at_safepoint,这是整个偏向锁向轻量级锁演进最核心的物理发生地。
cpp
// 文件物理路径: hotspot/src/share/vm/runtime/biasedLocking.cpp
BiasedLocking::Condition BiasedLocking::revoke_at_safepoint(Handle obj) {
assert(SafepointSynchronize::is_at_safepoint(), "must be executed at safepoint");
markOop mark = obj->mark();
// 再次核对,如果锁已经不是偏向状态,直接返回
if (!mark->has_bias_pattern()) {
return BIAS_REVOKED;
}
// 1. 获取当前持有该偏向锁的源线程指针 (线程 A)
JavaThread* biased_locker = mark->biased_locker();
// 如果偏向持有人为空(匿名偏向),直接将其擦除为普通无锁模式
if (biased_locker == NULL) {
obj->set_mark(markOopDesc::prototype()->copy_set_hash(mark->hash()));
return BIAS_REVOKED;
}
// 2. 核心探查:检查线程 A 是否依然存活
bool thread_is_alive = false;
// 遍历 JVM 全局线程链表,确认线程 A 未消亡
for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
if (thr == biased_locker) {
thread_is_alive = true;
break;
}
}
// 如果原偏向线程已经消亡,锁无法再被其持有,将其直接擦除为普通无锁状态(001)
if (!thread_is_alive) {
obj->set_mark(markOopDesc::prototype()->copy_set_hash(mark->hash()));
return BIAS_REVOKED;
}
// 3. 【核心骨架】:线程 A 依然存活,开始遍历线程 A 的调用栈,寻找是否依然在同步临界区内
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_locker);
BasicObjectLock* highest_lock = NULL;
bool WhosInMonitor = false;
// 倒序遍历线程 A 所有的栈帧(从栈顶到栈底)
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
// 判断当前栈帧关联的锁对象是否就是我们要撤销的 obj
if (mon_info->owner() == obj()) {
// 找到了线程 A 保存在其栈帧中的 Lock Record 空间
highest_lock = mon_info->lock();
WhosInMonitor = true; // 标志位:证明线程 A 依然待在 synchronized 临界区内
break;
}
}
// 核心分支 A: 线程 A 还活着,但是它已经执行完了同步块(不在临界区内)
if (!WhosInMonitor) {
// 既然 A 不再持有锁,直接将对象头还原为无锁状态 (001),擦除偏向指针
if (mark->has_bias_pattern()) {
obj->set_mark(markOopDesc::prototype()->copy_set_hash(mark->hash()));
}
return BIAS_REVOKED;
}
// 核心分支 B: 真正的锁升级发生地!线程 A 依然在同步块内持有此锁
// VM 线程在此处强行介入,代表线程 A 将其偏向锁重构为轻量级锁。
else {
// 1. 构造一个标准的无锁形态的 Mark Word (最后3位是 001) 作为基础
markOop prototype_header = markOopDesc::prototype()->copy_set_hash(mark->hash());
// 2. 将此无锁的 Mark Word 复制写入到线程 A 栈帧锁记录空间的 displaced_header 中
// 这完成了轻量级锁获取中最重要的第一步:栈顶留存原锁备份 (Displaced Mark Word)
highest_lock->set_displaced_header(prototype_header);
// 3. 【绝对核心点】:重写对象头。
// 将对象原本存储 54位线程ID|101 的 Mark Word 改写为指向最高层锁记录(highest_lock)的内存指针。
// 由于指针在 64位架构下是 8 字节对齐的,其最低两位天然为 00。
// 这一步直接将对象的锁状态在物理内存层面上修改为了 00(轻量级锁)。
obj->set_mark(markOopDesc::encode(highest_lock));
// 4. 处理递归锁情况
// 如果线程 A 在方法内部还存在对该对象的重入(多次 synchronized(obj)),
// 遍历后续的锁记录,将其 displaced_header 清空为 NULL,这是 HotSpot 轻量级锁重入的经典表征。
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
if (mon_info->owner() == obj() && mon_info->lock() != highest_lock) {
BasicObjectLock* lock = mon_info->lock();
lock->set_displaced_header(NULL); // 递归锁置空
}
}
return BIAS_REVOKED; // 升级完成
}
}
4. 栈帧级内存布局异动对比
为了更具象地展现上述核心分支 B 的"外科手术"成果,我们可以通过底层内存模型来观察其变化:
升级前(线程 A 持有偏向锁)
- 对象头 Mark Word:
[ 线程A的内存指针 (54位) | Epoch (2位) | Age (4位) | 1 | 01 ] - 线程 A 堆栈空间: 此时分配的
BasicObjectLock内部的displaced_header完全是一行无效零值,偏向锁模式下不使用此空间。
升级后(VM 线程在安全点内重写后)
- 对象头 Mark Word:
[ 线程 A 栈帧中 highest_lock 锁记录的内存地址 (62位) | 00 ] - 线程 A 堆栈空间: *
highest_lock->displaced_header内成功被写入了[ Unused (25位) | HashCode (31位) | Age (4位) | 0 | 01 ](即原对象的无锁备份)。 - 线程 A 的代码对这一切毫不知情。当它后续执行到退出同步块的汇编指令(
monitorexit)时,它会按照轻量级锁的释放逻辑,直接读取对象头指针并尝试通过 CAS 将displaced_header回写,完全无缝衔接。
5. 请求获取锁的线程 B 的后续命运
当虚拟机撤销操作完成并解除全局安全点(STW 结束)后,所有的 Java 线程恢复并发执行。此时发起撤销请求的线程 B 被唤醒,它从 VM_RevokeBias::execute 的等待中解脱,并接收到 BIAS_REVOKED 的返回结果。
回到 ObjectSynchronizer::fast_enter 中,由于未能直接斩获偏向锁,线程 B 顺流而下,直接调用 ObjectSynchronizer::slow_enter(obj, lock, THREAD):
cpp
// 文件物理路径: hotspot/src/share/vm/runtime/synchronizer.cpp
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
// 此时对象头已经被 VM 线程改写成了轻量级锁(00),不再匹配 is_neutral (无锁)
if (mark->is_neutral()) {
// 线程 B 无法进入此无锁快速分支
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
return;
}
}
// 检查是否为自己重入。此时轻量级锁的持有者是线程 A,mark->locker() 指向 A 的栈,不属于线程 B
else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
lock->set_displaced_header(NULL);
return;
}
// 结论:由于线程 A 依然霸占着轻量级锁,线程 B 在此处的 CAS 尝试必然遭受失败。
lock->set_displaced_header(markOopDesc::unused_mark());
// 线程 B 正式触发第二次锁升级:由当前的【轻量级锁】向【重量级锁 (ObjectMonitor)】膨胀
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
偏向锁向轻量级锁的升级,本质上是 JVM 利用安全点机制,将一个外部对象的全局状态(偏向指针)有保证地收拢、并物理重构为特定线程本地栈私有状态(Lock Record 指针) 的过程。这种设计用局部的、受控的 STW 停顿,换取了多线程在交替竞争时同步逻辑的绝对正确性。
总结:
偏向锁升级轻量级锁的过程图谱
为了让这一高频面试兼架构痛点更清晰,我们将上述源码逻辑收拢为以下的时序:
[ 线程 B ] [ JVM 核心 (User Mode) ] [ VM 线程 (Safepoint) ] [ 线程 A (原持有者) ]
| | | |
|-- 1.synchronized(obj) ------->| | |
| |-- 2.发现偏向线程 A ------------>| |
| | 提交 VM_RevokeBias 任务 | |
| | | |
|======================= 进入全局安全点 (STW) ====================| |
| | | |
| (被挂起) | |-- 3. 扫描线程 A 的调用栈 ---|
| | | |
| | |-- 4. 判定 A 仍在同步块内 |
| | | |
| | |-- 5. 修改 A 栈帧: |
| | | 填入 displaced_mark |
| | | |
| | |-- 6. 修改对象头: |
| | | 指向 A 栈帧, 标志设 00 |
| | | (至此跃升为轻量级锁) |
| | | |
|======================= 退出全局安全点 (Resume) =================| |
| | |
|-- 7. 从慢路径醒来 ------------>| |
| 执行 slow_enter() | |
| | |
|-- 8. 执行 CAS 抢轻量级锁 ----->| |
| (注: 必然失败, 因为被 A 占着) |
| | |
|-- 9. 触发最终防线 ------------>| |
| 调用 ObjectSynchronizer::inflate() 膨胀为重量级锁 |
v v v
系统视角的深度设计思考
从偏向锁向轻量级锁升级的设计,折射出 JVM 底层极其高超的并发哲学:
- 欺骗艺术(Transparent Escalation): 在 Safepoint 中直接重写正在运行的线程 A 的栈帧(Lock Record)和对象头,使得线程 A 在完全不知情的情况下,持有的锁类型发生了质变。A 醒来后,顺着原有的轻量级锁释放路径工作,两套机制完美闭环。
- Safepoint 的原罪: 偏向锁撤销需要遍历竞争线程的完整栈帧(
vframeStream),如果在高并发、强竞争(诸如多个线程频繁争抢同一个偏向锁)的场景下,锁频繁从偏向锁升级为轻量级锁,会引发大量的全局安全点停顿(STW)。 - 架构调优启示: 这也是为什么在微服务、高并发的生产环境中,系统工程师通常会明确加上
-XX:-UseBiasedLocking来禁用偏向锁。因为在注定存在竞争的体系内,直接从"无锁 -> 轻量级锁"开始,反而省去了撤销偏向锁带来的巨大 STW 性能开销。