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

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

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

  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 中的入口检测

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 底层极其高超的并发哲学:

  1. 欺骗艺术(Transparent Escalation): 在 Safepoint 中直接重写正在运行的线程 A 的栈帧(Lock Record)和对象头,使得线程 A 在完全不知情的情况下,持有的锁类型发生了质变。A 醒来后,顺着原有的轻量级锁释放路径工作,两套机制完美闭环。
  2. Safepoint 的原罪: 偏向锁撤销需要遍历竞争线程的完整栈帧(vframeStream),如果在高并发、强竞争(诸如多个线程频繁争抢同一个偏向锁)的场景下,锁频繁从偏向锁升级为轻量级锁,会引发大量的全局安全点停顿(STW)。
  3. 架构调优启示: 这也是为什么在微服务、高并发的生产环境中,系统工程师通常会明确加上 -XX:-UseBiasedLocking禁用偏向锁。因为在注定存在竞争的体系内,直接从"无锁 -> 轻量级锁"开始,反而省去了撤销偏向锁带来的巨大 STW 性能开销。
相关推荐
十月的皮皮1 小时前
C语言学习笔记20260611-水仙花数(2种解法)
c语言·笔记·学习
半部论语1 小时前
openEuler 安装 LibreOffice 技术指南
linux
码不停蹄的玄黓1 小时前
SpringBoot 实现拦截器
java·spring boot·后端
葱卤山猪1 小时前
二进制字节流序列化
c++·序列化
Lazionr1 小时前
类和对象(中):对象生命周期与运算符重载
c++
狗凯之家源码网1 小时前
永夜大圣 H5 棋牌大厅源码效果实测与品质解析
java·开发语言
凡人叶枫1 小时前
Effective C++ 条款13:以对象管理资源(RAII)
java·linux·开发语言·c++·嵌入式开发
小马爱打代码1 小时前
Java开发:Spring Cloud Alibaba微服务之消息队列(RocketMQ、Kafka、RabbitMQ)
java·java-rocketmq·java-rabbitmq
cfm_29141 小时前
JVM内存模型深度剖析与性能优化
jvm·性能优化