Java锁膨胀机制之偏向锁到轻量级锁源码剖析

发布时间:2026/6/11 18:45:22
Java锁膨胀机制之偏向锁到轻量级锁源码剖析 偏向锁到轻量级锁源码剖析前言偏向锁到轻量级锁源码剖析核心演进逻辑与状态机1. 系统视角的演进内核为什么转换不可轻视2. 偏向锁转轻量级锁的全局执行时序3. OpenJDK 8源码深度解析与详尽注释3.1 核心分流器synchronizer.cpp 中的入口检测3.2 运行时协调器biasedLocking.cpp 中的单线程尝试与 Safepoint 唤起3.3 核心手术台biasedLocking.cpp 中安全点内的栈帧重写4. 栈帧级内存布局异动对比升级前线程 A 持有偏向锁升级后VM 线程在安全点内重写后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 线程。到达全局安全点SafepointVM 线程响应请求挂起所有应用程序线程STW。此时全系统的内存状态处于绝对静态消除了数据竞争。VM 线程探查堆栈Stack WalkingVM 线程通过指针遍历线程 A 的所有栈帧Stack Frames寻找与当前锁对象关联的BasicObjectLock锁记录空间。锁状态内存重构核心升级点场景一若线程 A 已经退出了同步块或已消亡VM 线程直接将对象头重置为普通的无锁状态001。安全点结束后线程 B 按照正常的轻量级锁路径通过 CAS 压入自己的锁记录。场景二若线程 A 依然在同步块中VM 线程代表线程 A 执行轻量级锁的构建工作。它将对象原本的无锁 Mark WordDisplaced 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中的入口检测// 文件物理路径: hotspot/src/share/vm/runtime/synchronizer.cppvoidObjectSynchronizer::fast_enter(Handle obj,BasicLock*lock,boolattempt_rebias,TRAPS){if(UseBiasedLocking){// 确保当前不处于安全点正常的业务线程执行路径if(!SafepointSynchronize::is_at_safepoint()){// 核心调用尝试撤销并重新偏向。// 对于多线程竞争场景此函数内部会因为无法即时处理而向 VM 线程申请 SafepointBiasedLocking::Condition condBiasedLocking::revoke_and_rebias(obj,attempt_rebias,THREAD);// 如果返回 BIAS_REVOKED_AND_REBIASED说明是匿名偏向成功或重偏向成功直接返回if(condBiasedLocking::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 唤起// 文件物理路径: hotspot/src/share/vm/runtime/biasedLocking.cppBiasedLocking::ConditionBiasedLocking::revoke_and_rebias(Handle obj,boolattempt_rebias,TRAPS){assert(!SafepointSynchronize::is_at_safepoint(),must not be called at safepoint);markOop markobj-mark();// 检查对象是否为偏向锁模式 (低3位是否为 101)if(mark-has_bias_pattern()){JavaThread*biased_lockermark-biased_locker();// 如果 biased_locker 指针不为主说明该锁当前已被某个具体线程持有if(biased_locker!NULL){// 场景 A: 锁虽然是偏向锁但持有者就是当前线程自己if(biased_lockerTHREAD){// 属于偏向锁重入汇编层未命中时进入此处直接返回成功returnBIAS_REVOKED_AND_REBIASED;}// 场景 B: 核心竞争点偏向持有人是线程 A而当前请求获取锁的是线程 B// 此时线程 B 必须强制撤销线程 A 的偏向状态由于涉及跨线程修改对方的执行上下文明细// 线程 B 无法单方面完成必须依赖 VM 线程投递一个安全点任务VM_Operation。// 构造一个安全点撤销偏向的任务投递给底层 VMThreadVM_RevokeBiasrevoke_op(obj,THREAD);VMThread::execute(revoke_op);// 此处会触发 STW 挂起所有线程直至 VM 线程处理完毕// 安全点结束后当前业务线程被唤醒读取 VM 线程留下的处理状态returnrevoke_op.result();}}returnBIAS_REVOKED;}3.3 核心手术台biasedLocking.cpp中安全点内的栈帧重写当所有线程在 Safepoint 陷入静止后VM 线程开始执行BiasedLocking::revoke_at_safepoint这是整个偏向锁向轻量级锁演进最核心的物理发生地。// 文件物理路径: hotspot/src/share/vm/runtime/biasedLocking.cppBiasedLocking::ConditionBiasedLocking::revoke_at_safepoint(Handle obj){assert(SafepointSynchronize::is_at_safepoint(),must be executed at safepoint);markOop markobj-mark();// 再次核对如果锁已经不是偏向状态直接返回if(!mark-has_bias_pattern()){returnBIAS_REVOKED;}// 1. 获取当前持有该偏向锁的源线程指针 (线程 A)JavaThread*biased_lockermark-biased_locker();// 如果偏向持有人为空匿名偏向直接将其擦除为普通无锁模式if(biased_lockerNULL){obj-set_mark(markOopDesc::prototype()-copy_set_hash(mark-hash()));returnBIAS_REVOKED;}// 2. 核心探查检查线程 A 是否依然存活boolthread_is_alivefalse;// 遍历 JVM 全局线程链表确认线程 A 未消亡for(JavaThread*thrThreads::first();thr!NULL;thrthr-next()){if(thrbiased_locker){thread_is_alivetrue;break;}}// 如果原偏向线程已经消亡锁无法再被其持有将其直接擦除为普通无锁状态001if(!thread_is_alive){obj-set_mark(markOopDesc::prototype()-copy_set_hash(mark-hash()));returnBIAS_REVOKED;}// 3. 【核心骨架】线程 A 依然存活开始遍历线程 A 的调用栈寻找是否依然在同步临界区内GrowableArrayMonitorInfo**cached_monitor_infoget_or_compute_monitor_info(biased_locker);BasicObjectLock*highest_lockNULL;boolWhosInMonitorfalse;// 倒序遍历线程 A 所有的栈帧从栈顶到栈底for(inti0;icached_monitor_info-length();i){MonitorInfo*mon_infocached_monitor_info-at(i);// 判断当前栈帧关联的锁对象是否就是我们要撤销的 objif(mon_info-owner()obj()){// 找到了线程 A 保存在其栈帧中的 Lock Record 空间highest_lockmon_info-lock();WhosInMonitortrue;// 标志位证明线程 A 依然待在 synchronized 临界区内break;}}// 核心分支 A: 线程 A 还活着但是它已经执行完了同步块不在临界区内if(!WhosInMonitor){// 既然 A 不再持有锁直接将对象头还原为无锁状态 (001)擦除偏向指针if(mark-has_bias_pattern()){obj-set_mark(markOopDesc::prototype()-copy_set_hash(mark-hash()));}returnBIAS_REVOKED;}// 核心分支 B: 真正的锁升级发生地线程 A 依然在同步块内持有此锁// VM 线程在此处强行介入代表线程 A 将其偏向锁重构为轻量级锁。else{// 1. 构造一个标准的无锁形态的 Mark Word (最后3位是 001) 作为基础markOop prototype_headermarkOopDesc::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(inti0;icached_monitor_info-length();i){MonitorInfo*mon_infocached_monitor_info-at(i);if(mon_info-owner()obj()mon_info-lock()!highest_lock){BasicObjectLock*lockmon_info-lock();lock-set_displaced_header(NULL);// 递归锁置空}}returnBIAS_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)// 文件物理路径: hotspot/src/share/vm/runtime/synchronizer.cppvoidObjectSynchronizer::slow_enter(Handle obj,BasicLock*lock,TRAPS){markOop markobj-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;}}// 检查是否为自己重入。此时轻量级锁的持有者是线程 Amark-locker() 指向 A 的栈不属于线程 Belseif(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 性能开销。