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()条件队列改造)
- [1. `ObjectMonitor` 所有权编码重构 (`objectMonitor.hpp`)](#1.
- [四、 轻量级锁重构:Thread-Local Lock Stack 机制](#四、 轻量级锁重构:Thread-Local Lock Stack 机制)
- [五、 HotSpot 栈冻结(`Continuation::freeze`)的拦截解除](#五、 HotSpot 栈冻结(
Continuation::freeze)的拦截解除) - [六、 演进前后对比与工程价值](#六、 演进前后对比与工程价值)
-
- [1. 技术架构演进对比](#1. 技术架构演进对比)
- [2. 生产工程价值](#2. 生产工程价值)
- [一、 根因剖析:为什么 JDK 21 中 `synchronized` 会导致 Pinning?](#一、 根因剖析:为什么 JDK 21 中
前言
本文旨在记录近期研读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 调度其他任务!
架构上的四大关键改变:
_owner字段编码重构 :能够区分JavaThread*、VirtualThread oop以及匿名锁状态。ObjectWaiter与队列重构 :等待队列(_cxq/_EntryList/_WaitSet)支持保存虚拟线程句柄。- 阻塞/唤醒机制路由 :抢锁失败时, platform 线程走
PlatformEvent::park(),virtual 线程走VirtualThread.park()(即Continuation::yield())。 - 轻量级锁脱离 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 |
+-------------------------------+ +-----------------------+
- 锁对象直接入栈 :当执行轻量级加锁(Fast Path)时,JVM 仅仅是将加锁对象的
oop(内存地址)推入当前线程的_lock_stack数组中,并在对象的 Mark Word 中设置轻量级锁标记。 - 剥离 Native Stack 依赖 :锁状态不再依赖 Native Stack 上某个
BasicLock变量的物理内存地址(RSP/RBP 偏移)。 - 支持栈帧 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. 生产工程价值
- 零成本迁移存量代码 :以往为了适配虚拟线程,企业不得不使用
ReentrantLock大面积重构遗留代码库(如 Spring 早期版本、MySQL JDBC 驱动、旧版 HttpClient)。在 JDK 23/24 中,传统的synchronized恢复为其原有的"第一公民"并发原语地位,性能与ReentrantLock看齐。 - 彻底解决 Carrier 线程枯竭(Exhaustion) :在百万级并发微服务场景下,不再需要设置
-Djdk.virtualThreadScheduler.maxPoolSize来防止 Carrier 线程被synchronized耗尽。 - 监视器诊断与 JFR 统一 :
synchronized的监视器事件与 JFR (jdk.JavaMonitorEnter) 无缝融合,能够精准呈现 VirtualThread 的真实等待时长,而不会误报 Carrier 线程停顿。