Java锁膨胀机制之偏向锁到轻量级锁源码剖析
2026/6/11 18:44:24 网站建设 项目流程

偏向锁到轻量级锁源码剖析

  • 前言
  • 偏向锁到轻量级锁源码剖析
    • 核心演进逻辑与状态机
    • 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 的当前状态,决定将其降级为无锁,还是直接就地升级为轻量级锁:

  1. 交替执行(非真正竞争):如果线程 A 已经退出了同步块(不处于存活状态,或者虽然存活但已经释放了锁),JVM 会将对象头恢复为普通无锁状态(001)。随后,线程 B 通过正常的轻量级锁 CAS 逻辑获取锁,锁升级为轻量级锁(00)。
  2. 激进竞争(真正竞争):如果线程 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. 偏向锁转轻量级锁的全局执行时序

  1. 多线程交替/竞争检测:线程 B 在ObjectSynchronizer::fast_enter路径中发现对象处于偏向锁状态(101),且偏向线程指针指向线程 A(而非自己)。
  2. 发起撤销请求:线程 B 调用BiasedLocking::revoke_and_rebias。由于无法直接修改线程 A 的状态,且无法判定 A 是否存活或正在同步块内,线程 B 构造一个VM_RevokeBias操作并提交给 VM 线程。
  3. 到达全局安全点(Safepoint):VM 线程响应请求,挂起所有应用程序线程(STW)。此时,全系统的内存状态处于绝对静态,消除了数据竞争。
  4. VM 线程探查堆栈(Stack Walking):VM 线程通过指针遍历线程 A 的所有栈帧(Stack Frames),寻找与当前锁对象关联的BasicObjectLock(锁记录空间)。
  5. 锁状态内存重构(核心升级点):
  • 场景一:若线程 A 已经退出了同步块(或已消亡):VM 线程直接将对象头重置为普通的无锁状态(001)。安全点结束后,线程 B 按照正常的轻量级锁路径,通过 CAS 压入自己的锁记录。
  • 场景二:若线程 A 依然在同步块中:VM 线程代表线程 A 执行轻量级锁的构建工作。它将对象原本的无锁 Mark Word(Displaced Mark Word)写入线程 A 最高层栈帧的锁记录中,然后将对象头的 Mark Word 修改为指向线程 A 栈帧锁记录的指针,并将锁标志位置为00
  1. 安全点解除与滚入慢路径:恢复执行后,线程 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 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 唤起

// 文件物理路径: 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 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){// 属于偏向锁重入,汇编层未命中时进入此处,直接返回成功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 mark=obj->mark();// 再次核对,如果锁已经不是偏向状态,直接返回if(!mark->has_bias_pattern()){returnBIAS_REVOKED;}// 1. 获取当前持有该偏向锁的源线程指针 (线程 A)JavaThread*biased_locker=mark->biased_locker();// 如果偏向持有人为空(匿名偏向),直接将其擦除为普通无锁模式if(biased_locker==NULL){obj->set_mark(markOopDesc::prototype()->copy_set_hash(mark->hash()));returnBIAS_REVOKED;}// 2. 核心探查:检查线程 A 是否依然存活boolthread_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()));returnBIAS_REVOKED;}// 3. 【核心骨架】:线程 A 依然存活,开始遍历线程 A 的调用栈,寻找是否依然在同步临界区内GrowableArray<MonitorInfo*>*cached_monitor_info=get_or_compute_monitor_info(biased_locker);BasicObjectLock*highest_lock=NULL;boolWhosInMonitor=false;// 倒序遍历线程 A 所有的栈帧(从栈顶到栈底)for(inti=0;i<cached_monitor_info->length();i++){MonitorInfo*mon_info=cached_monitor_info->at(i);// 判断当前栈帧关联的锁对象是否就是我们要撤销的 objif(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()));}returnBIAS_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(inti=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);// 递归锁置空}}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 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 的栈,不属于线程 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 底层极其高超的并发哲学:

  1. 欺骗艺术(Transparent Escalation):在 Safepoint 中直接重写正在运行的线程 A 的栈帧(Lock Record)和对象头,使得线程 A 在完全不知情的情况下,持有的锁类型发生了质变。A 醒来后,顺着原有的轻量级锁释放路径工作,两套机制完美闭环。
  2. Safepoint 的原罪:偏向锁撤销需要遍历竞争线程的完整栈帧(vframeStream),如果在高并发、强竞争(诸如多个线程频繁争抢同一个偏向锁)的场景下,锁频繁从偏向锁升级为轻量级锁,会引发大量的全局安全点停顿(STW)。
  3. 架构调优启示:这也是为什么在微服务、高并发的生产环境中,系统工程师通常会明确加上-XX:-UseBiasedLocking禁用偏向锁。因为在注定存在竞争的体系内,直接从“无锁 -> 轻量级锁”开始,反而省去了撤销偏向锁带来的巨大 STW 性能开销。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询