JEP491中synchronized关键字引发的平台线程钉住解决方法简介

JEP491中synchronized关键字引发的平台线程钉住解决方法简介

  • 前言
  • synchronized关键字引发的平台线程钉住解决方法
    • [一、 根因剖析:为什么 JDK 21 中 `synchronized` 会导致 Pinning?](#一、 根因剖析:为什么 JDK 21 中 synchronized 会导致 Pinning?)
      • [1. 锁记录(Lock Record)绑定 Native Stack](#1. 锁记录(Lock Record)绑定 Native Stack)
      • [2. `ObjectMonitor::_owner` 强绑定 `JavaThread*`](#2. ObjectMonitor::_owner 强绑定 JavaThread*)
      • [3. 阻塞原语强绑定 OS 线程(`ParkEvent::park`)](#3. 阻塞原语强绑定 OS 线程(ParkEvent::park))
    • [二、 JDK 23/24 JEP 491 核心重构方案概览](#二、 JDK 23/24 JEP 491 核心重构方案概览)
    • [三、 C++ 源码级深度剖析](#三、 C++ 源码级深度剖析)
      • [1. `ObjectMonitor` 所有权编码重构 (`objectMonitor.hpp`)](#1. ObjectMonitor 所有权编码重构 (objectMonitor.hpp))
      • [2. 抢锁与 Yield 路径 (`ObjectMonitor::enter`)](#2. 抢锁与 Yield 路径 (ObjectMonitor::enter))
      • [3. 锁释放与 Unpark 调度路径 (`ObjectMonitor::exit`)](#3. 锁释放与 Unpark 调度路径 (ObjectMonitor::exit))
      • [4. `Object.wait()` 与 `notify()` 条件队列改造](#4. Object.wait() 与 notify() 条件队列改造)
    • [四、 轻量级锁重构:Thread-Local Lock Stack 机制](#四、 轻量级锁重构:Thread-Local Lock Stack 机制)
    • [五、 HotSpot 栈冻结(`Continuation::freeze`)的拦截解除](#五、 HotSpot 栈冻结(Continuation::freeze)的拦截解除)
    • [六、 演进前后对比与工程价值](#六、 演进前后对比与工程价值)
      • [1. 技术架构演进对比](#1. 技术架构演进对比)
      • [2. 生产工程价值](#2. 生产工程价值)

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

synchronized关键字引发的平台线程钉住解决方法

在 JDK 21 中,Project Loom 带来虚拟线程(Virtual Thread)时,synchronized 关键字导致的 Pinning(线程钉住) 是最主要的性能隐患------只要虚拟线程在 synchronized 块或方法内部触发了阻塞操作(如网络 I/O、LockSupport.park()、Object.wait()),底层的 Carrier 平台线程就会被硬性锁定并随之阻塞,无法被释放去调度其他虚拟线程。

为了彻底解决这一痛点,OpenJDK 团队在 JDK 23 中推出了 JEP 491 (Synchronized With Virtual Threads) ,并在 JDK 24 中进一步完善。这项重构对 HotSpot 运行时的 ObjectMonitor、锁膨胀机制(Lock Inflation)、轻量级锁(Lightweight Locking)以及 Continuation 栈冻结(Freeze)逻辑进行了深度的 C++ 源码级改造。

以下是对这一重构方案的底层技术实现剖析。


一、 根因剖析:为什么 JDK 21 中 synchronized 会导致 Pinning?

在 JDK 21 及更早的 HotSpot 实现中,synchronized 锁的实现深度依赖于 Carrier 线程的 Native Stack 以及 特定平台线程绑定的 C++ 数据结构:

1. 锁记录(Lock Record)绑定 Native Stack

在无膨胀的轻量级锁(Fast Path)阶段,JVM 会在当前线程的 Native Stack 栈帧中分配一个 BasicObjectLock / BasicLock(锁记录)。

  • 当虚拟线程执行 Continuation::freeze(栈帧冻结)时,这些位于 Native Stack 上的物理指针在拷贝到堆内的 stackChunkOop 后会发生地址偏移。
  • 传统的轻量级锁依赖栈帧指针(RBP/RSP 相对位置)进行锁重入计数与解锁,一旦解除挂载(Unmount)并在另一个 Carrier 线程上解冻(Thaw),Native Stack 地址发生变化,锁记录指针直接失效。

2. ObjectMonitor::_owner 强绑定 JavaThread*

当锁膨胀为重量级锁(Slow Path)时,C++ 类 ObjectMonitor 的 _owner 字段直接存放持有锁的 Thread* 或 JavaThread* 指针(即底层 OS Carrier 线程的 C++ 对象指针)。

  • ObjectMonitor 无法感知当前持有锁的是一个逻辑上的 VirtualThread 还是物理上的 JavaThread。
  • 若 _owner 记录的是 Carrier 线程,一旦虚拟线程 Unmount,其他 Carrier 线程尝试抢锁时,物理 _owner 指针的语义就会发生混乱。

3. 阻塞原语强绑定 OS 线程(ParkEvent::park)

在 ObjectMonitor::enter() 或 Object.wait() 争抢锁失败时,传统 HotSpot 会调用 Thread::current()->_ParkEvent->park(),直接触发操作系统级别的阻塞(如 Linux 的 sys_futex 或 pthread_cond_wait)。这直接冻结了底层的 Carrier OS 线程,使其无法退回 ForkJoinPool。

复制代码
【JDK 21 阻塞路径(导致 Pinning)】
VirtualThread -> synchronized -> ObjectMonitor::enter()
                      | (争抢失败)
                      v
            ObjectWaiter::park()
                      |
                      v
          PlatformEvent::park() -> Linux sys_futex() (Carrier OS 线程被硬性挂起)

二、 JDK 23/24 JEP 491 核心重构方案概览

JEP 491 的核心思想是:**将 ObjectMonitor 的所有权、阻塞队列与底层的物理 OS 线程彻底解耦,使其能够识别 VirtualThread 对象,并将 OS 级的线程阻塞替换为用户态的 Continuation::yield()**。

复制代码
【JDK 23/24 重构后的阻塞路径(无 Pinning)】
VirtualThread -> synchronized -> ObjectMonitor::enter()
                      | (争抢失败)
                      v
          判断当前 Thread 为 VirtualThread
                      |
                      v
      入队 _cxq / _EntryList (挂载 ObjectWaiter 节点)
                      |
                      v
      VirtualThread.park() -> Continuation::yield() (Unmount 卸载)
                      |
      Carrier 线程释放,返回 ForkJoinPool 调度其他任务!

架构上的四大关键改变:

  1. _owner 字段编码重构 :能够区分 JavaThread*、VirtualThread oop 以及匿名锁状态。
  2. ObjectWaiter 与队列重构 :等待队列(_cxq / _EntryList / _WaitSet)支持保存虚拟线程句柄。
  3. 阻塞/唤醒机制路由 :抢锁失败时, platform 线程走 PlatformEvent::park(),virtual 线程走 VirtualThread.park() (即 Continuation::yield())。
  4. 轻量级锁脱离 Native Stack :全面引入并完善 Thread-Local Lock Stack(线程本地锁栈) 机制。

三、 C++ 源码级深度剖析

1. ObjectMonitor 所有权编码重构 (objectMonitor.hpp)

在 JDK 23/24 的 HotSpot 源码中,ObjectMonitor 内部的 _owner 字段不再是一个单纯的 void* 或 Thread*,而是采用了 Tagged Pointer / 多态编码:

cpp 复制代码
// src/hotspot/share/runtime/objectMonitor.hpp (JDK 23/24)

class ObjectMonitor {
  // ...
  // _owner 现在可以存储:
  // 1. nullptr                       : 无锁状态
  // 2. JavaThread*                   : 平台线程持有锁
  // 3. oop (VirtualThread 的 Java 对象) : 虚拟线程持有锁
  // 4. ANONYMOUS_OWNER               : 轻量级锁设置的匿名持有标记
  // 5. DEFLATED_MARKER               : 监视器消退标记
  std::atomic<intptr_t> _owner;
  
  // 辅助 API:提取与判断真实持有者
  inline bool is_owner_anonymous() const;
  inline bool has_owner() const;
  inline void* owner() const;
  inline oop owner_oop() const; // 若持有者为 VirtualThread,返回其 oop (Object OOP)
};

通过这一改造,HotSpot 能够精准知道"究竟是哪个虚拟线程持有该锁"。即使虚拟线程发生了 Unmount,_owner 中保存的虚拟线程 oop 在堆内依然合法,且能被 GC 屏障正确追踪与更新。

2. 抢锁与 Yield 路径 (ObjectMonitor::enter)

在 src/hotspot/share/runtime/objectMonitor.cpp 中,当线程试图获取膨胀锁失败时,enter 方法的分支逻辑发生了根本改变:

cpp 复制代码
// HotSpot 源码伪代码逻辑 (JDK 23/24 演进)
void ObjectMonitor::enter(TRAPS) {
  JavaThread* current = JavaThread::current();

  // 1. 尝试 CAS 抢锁
  if (try_enter(current)) {
    return; // 抢锁成功
  }

  // 2. 抢锁失败,构造等待节点
  ObjectWaiter node(current);
  
  // 判断当前线程是否在运行 VirtualThread
  VThread* vthread = current->vthread_object(); 
  if (vthread != nullptr) {
    node._is_virtual = true;
    node._vthread_oop = vthread; // 绑定虚拟线程的 oop
  }

  // 3. 将 node 原子入队到 _cxq (Contention Queue)
  AddWaiterToQueue(&node);

  // 4. 阻塞等待循环
  for (;;) {
    if (try_lock_or_steal()) {
      break; // 被唤醒后成功抢到锁
    }

    if (node._is_virtual) {
      // ===== JEP 491 核心突破点 =====
      // 虚拟线程路径:不调用 OS 原生 park,而是触发 JVM 用户态 park
      // 这将在底层引发 Continuation::yield(),将当前栈帧 Freeze 到堆内并 Unmount
      current->vthread_park(); 
    } else {
      // 平台线程路径:保留原有的 OS 级 ParkEvent
      current->_ParkEvent->park(); 
    }
  }

  // 5. 抢锁成功,出队并恢复状态
  UnlinkWaiterFromQueue(&node);
}

3. 锁释放与 Unpark 调度路径 (ObjectMonitor::exit)

当锁持有者(无论是平台线程还是虚拟线程)退出 synchronized 块并调用 ObjectMonitor::exit() 时,它需要唤醒 _EntryList 或 _cxq 中的下一个等待节点:

cpp 复制代码
// src/hotspot/share/runtime/objectMonitor.cpp
void ObjectMonitor::exit(bool c2_set_owner, TRAPS) {
  // ... 清理 _owner 标识 ...

  // 获取队列中的下一个 Waiter 节点
  ObjectWaiter* w = dequeue_next_waiter();
  if (w == nullptr) return;

  if (w->_is_virtual) {
    // ===== JEP 491 虚拟线程唤醒 =====
    // 提取等待节点中保存的 VirtualThread oop
    oop vthread_oop = w->_vthread_oop;
    
    // 调用 Java 层的 LockSupport.unpark(vthread) 逻辑
    // 将该 VirtualThread 重新提交给 ForkJoinPool 任务队列,等待 Carrier 调度
    JavaThread::unpark_virtual_thread(vthread_oop);
  } else {
    // 平台线程唤醒:直接触发 PlatformEvent 信号
    w->_event->unpark();
  }
}

4. Object.wait() 与 notify() 条件队列改造

Object.wait() 传统上会导致 OS 线程在 _WaitSet 队列对应的 ParkEvent 上挂起。JEP 491 重新整合了 WaitSet 机制:

  • wait() 过程 :虚拟线程将自身作为 ObjectWaiter 插入 _WaitSet,释放 ObjectMonitor 的所有权,随后触发 Continuation::yield() 进行 Unmount。
  • notify() / notifyAll() 过程 :通知者将 ObjectWaiter 从 _WaitSet 移至 _EntryList 或 _cxq。
  • 重新抢锁 :被 notify 的虚拟线程唤醒后,重新进入 ObjectMonitor::enter() 的竞争逻辑。整个过程 Carrier 线程零阻塞。

四、 轻量级锁重构:Thread-Local Lock Stack 机制

除重量级锁(ObjectMonitor)外,HotSpot 在 JDK 21+ 引入并在 JDK 23 中完善了 Lightweight Locking(JEP 428/458 演进) ,彻底弃用了依靠 Native Stack 记录锁地址的 BasicObjectLock 链表。

核心设计:JavaThread::_lock_stack

HotSpot 在 JavaThread C++ 对象中直接置入了一个固定大小的指针数组 LockStack(锁栈):

cpp 复制代码
class JavaThread : public Thread {
private:
  // Thread-Local Lock Stack (JDK 21+ 引入,JDK 23 重构)
  LockStack _lock_stack; 
};
复制代码
Native Stack (无 Lock Record 强绑定)          JavaThread (C++ 对象)
+-------------------------------+             +-----------------------+
| Frame N (c2_compiled)         |             | _lock_stack (Array)   |
|   - 压栈/弹栈仅触发汇编指令   |             |  [0] -> oop (Object A)|
|     更新 JavaThread 锁栈      | ----------> |  [1] -> oop (Object B)|
| Frame N-1                     |             |  [2] -> nullptr       |
+-------------------------------+             +-----------------------+
  1. 锁对象直接入栈 :当执行轻量级加锁(Fast Path)时,JVM 仅仅是将加锁对象的 oop(内存地址)推入当前线程的 _lock_stack 数组中,并在对象的 Mark Word 中设置轻量级锁标记。
  2. 剥离 Native Stack 依赖 :锁状态不再依赖 Native Stack 上某个 BasicLock 变量的物理内存地址(RSP/RBP 偏移)。
  3. 支持栈帧 Freeze/Thaw :在虚拟线程 Unmount 时,HotSpot 只需把 _lock_stack 中的对象引用暂存至虚拟线程对应的堆内存数据结构中;Mount 时再复原至 Carrier 的 _lock_stack 即可。这使得轻量级锁在 Fast Path 下同样能够无缝切出。

五、 HotSpot 栈冻结(Continuation::freeze)的拦截解除

在 JDK 21 中,Continuation::freeze 在执行栈漫游(Stack Walking)之前,会检查当前线程是否持有 Monitor 锁或处于 synchronized 方法内部。如果检查到,直接抛出 Pinning 异常或终止 Unmount。

在 JDK 23/24 JEP 491 的实现中,Continuation::freeze 的检查逻辑被大幅放宽:

cpp 复制代码
// src/hotspot/share/runtime/continuationFreezeThaw.cpp (JDK 23/24 逻辑演进)

FreezeResult Continuation::is_pinned(JavaThread* thread) {
  // JDK 21 原有逻辑:
  // if (thread->has_monitors() || thread->is_in_synchronized_segment()) {
  //     return PINNED_MONITOR; // 强制 Pinning!
  // }

  // JDK 23/24 JEP 491 逻辑:
  // 移除了对 ObjectMonitor 的硬性 Pinning 检查!
  // 仅在以下极端不可避免的场景保留 Pinning:
  if (thread->has_native_frames()) {
    return PINNED_NATIVE_FRAME; // 包含 JNI / Native C 代码栈帧
  }
  
  return UNPINNED; // 允许 Freeze/Unmount!
}

六、 演进前后对比与工程价值

1. 技术架构演进对比

维度 JDK 21 (旧实现) JDK 23 / 24 (JEP 491 重构实现)
synchronized 与 Unmount 强制 Pinning。 Carrier 线程随之物理阻塞。 完全解耦。允许虚拟线程切出(Unmount)。
ObjectMonitor::_owner 含义 仅存储物理 JavaThread* 指针。 多态 Tagged Pointer(支持 VirtualThread oop)。
争抢失败阻塞机制 调用 PlatformEvent::park() (Linux sys_futex)。 路由至 VirtualThread.park() (Continuation::yield())。
轻量级锁实现 绑定 Native Stack 帧上的 BasicObjectLock。 基于 JavaThread::_lock_stack (Thread-Local 锁栈)。
唤醒机制 OS 级 pthread_cond_signal / futex 唤醒。 触发 LockSupport.unpark(vthread),交由 ForkJoinPool 调度。

2. 生产工程价值

  1. 零成本迁移存量代码 :以往为了适配虚拟线程,企业不得不使用 ReentrantLock 大面积重构遗留代码库(如 Spring 早期版本、MySQL JDBC 驱动、旧版 HttpClient)。在 JDK 23/24 中,传统的 synchronized 恢复为其原有的"第一公民"并发原语地位,性能与 ReentrantLock 看齐。
  2. 彻底解决 Carrier 线程枯竭(Exhaustion) :在百万级并发微服务场景下,不再需要设置 -Djdk.virtualThreadScheduler.maxPoolSize 来防止 Carrier 线程被 synchronized 耗尽。
  3. 监视器诊断与 JFR 统一 :synchronized 的监视器事件与 JFR (jdk.JavaMonitorEnter) 无缝融合,能够精准呈现 VirtualThread 的真实等待时长,而不会误报 Carrier 线程停顿。
相关推荐
All for pursuit.1 小时前
【贪心-4】581.最短无序连续子数组
数据结构·c++·算法·leetcode
应用市场1 小时前
嵌入式Linux从裸板到产品(一):全景地图与交叉编译环境,从四个上板翻车现场说
linux·运维·服务器
皓月盈江1 小时前
Linux系统PC与Linux系统服务器通过scp上传与下载文件
linux·运维·服务器·scp·文件上传·文件下载
Ruiery1 小时前
Linux 6.6内核 CPU 启动深度解析(二):AP 拉起 — BSP 如何用 INIT-SIPI 唤醒其余 CPU
linux·运维·服务器
xcLeigh1 小时前
【KingbaseES数据库教程】国产化信创背景下的数据库选型与初识
linux·数据库·windows·kes·数据选型
rm -rf * && haha.sh1 小时前
【Linux】Rocky 9.8 操作系统磁盘分区与MySQL数据目录迁移
linux·运维·mysql
无名猿1 小时前
list 与 forward_list:链表真的比 vector 快吗
c++·性能优化·stl·内存管理·标准库
笑鸿的学习笔记2 小时前
C++笔记之SIMD与SSE指令集
开发语言·c++·笔记
anew___2 小时前
《从零手写操作系统 (20):ELF加载器——让OS读懂现代编译器》
java·服务器·前端