揭秘 Java 虚拟线程 Pinning:底层触发点与 JDK 24 的终极救赎
如果你正在使用 JDK 21 的虚拟线程,那么 Pinning 是你绕不开的一课。
如果你准备升级到 JDK 24,那么这篇文章会告诉你——游戏规则已经变了。
一、虚拟线程真正的本质:Continuation + 可卸载栈
虚拟线程(Virtual Thread, VT)之所以“便宜”,不是因为它更快。
而是因为:
它可以在阻塞时从平台线程(Carrier Thread)上卸载(Unmount)。
底层机制依赖:
Continuation.freeze() Continuation.mount()
当 VT 遇到阻塞点:
-
JVM 尝试冻结当前调用栈(拷贝 stack chunk 到堆)
-
将 VT 与 Carrier 解绑定
-
Carrier 去执行其他 VT
这叫:
协作式调度(User-mode scheduling)
而 Pinning 的本质是:
当 JVM 发现当前栈无法安全冻结时,freeze 失败,虚拟线程被“钉”在 Carrier 上。
二、Pinning 的本质
Pinning 不是锁竞争。
Pinning 是:
freezeContinuation()
返回失败
当出现以下情况时:
-
持有不可迁移资源
-
存在不可复制栈帧
-
进入 JVM 关键区
JVM 会拒绝卸载虚拟线程。
于是:
-
VT 阻塞
-
Carrier 也被占住
-
系统吞吐下降
如果 Pin 频繁发生:
虚拟线程会退化成 1:1 平台线程。
三、触发点一:synchronized 的历史包袱(JDK 21/23)
为什么 synchronized 会 Pin?
在 JDK 24 之前:
ObjectMonitor.owner = JavaThread
注意这个 JavaThread 是绑定 OS 线程的。
当你写:
synchronized (lock) { Thread.sleep(1000); }
发生的底层过程是:
-
Monitor.owner 绑定当前 Carrier
-
VT 遇到阻塞
-
JVM 尝试 freeze
-
检查发现持有 Monitor
-
无法保证锁语义跨 Carrier 正确
-
freeze 失败 → Pin
核心问题:
Monitor 设计假设:线程身份 = OS 线程身份
而虚拟线程打破了这个假设。
为什么 ReentrantLock 不会 Pin?
ReentrantLock 基于:
AbstractQueuedSynchronizer (AQS)
阻塞依赖:
LockSupport.park()
而 park 是 Continuation 安全点。
AQS 只依赖“逻辑线程”,不依赖 OS 线程身份。
所以:
-
park 可卸载
-
Monitor 不可卸载(JDK 24 之前)
这是设计哲学差异。
四、触发点二:Native / JNI / FFM
这个问题到 JDK 24 仍然存在。
原因很简单:
C 栈无法被 JVM 冻结。
当 VT 进入:
-
JNI
-
FFM (Foreign Function & Memory API)
-
本地阻塞 IO
执行流跨越 Java → Native 边界。
此时:
-
栈在 C 层
-
JVM 无法复制
-
也无法安全迁移
因此:
只要 Native 中发生阻塞 => 必然 Pin
这是结构性限制。
五、JDK 24 的重大突破:JEP 491
JEP 491 做了一件非常硬核的事情:
重写了 Object Monitor 的“所有者模型”。
核心变化:
旧模型
Monitor.owner = PlatformThread
新模型
Monitor.owner = VirtualThread
也就是说:
-
锁状态跟随 VT
-
而不再绑定 Carrier
这意味着:
当 VT 在 synchronized 内阻塞:
-
可以安全卸载
-
Monitor 状态随 VT 挂起
-
恢复时继续持有锁
这彻底解决了:
synchronized + 阻塞 = Pin
六、JDK 24 之后还会 Pin 吗?
会。
但范围缩小到:
-
Native 调用
-
FFM
-
JVM 内部关键区
-
某些 GC / Safepoint 区域
换句话说:
Java 层的锁已经安全,Native 仍然是孤岛。
七、底层视角:Pin 发生在哪个函数?
在 HotSpot 中:
关键路径是:
VirtualThread::yieldContinuation()
内部会调用:
Continuation.freeze()
freeze 会检查:
-
是否持有 Monitor(JDK 21)
-
是否存在 Native frame
-
是否在 critical section
-
是否处于 safepoint
如果条件不满足:
Continuation::pin()
标记 pinned 状态。
这不是语法问题。
这是 JVM 级调度决策。
八、如何监控 Pin?
JFR
查看:
jdk.VirtualThreadPinned
可以看到:
-
堆栈
-
持续时间
-
发生位置
这是生产环境首选方式。
启动参数(JDK 21/23)
-Djdk.tracePinnedThreads=full
控制台会打印 Pin 栈信息。
注意:
在 JDK 24 中:
-
synchronized 不再触发
-
只剩 Native 等情况
九、一个更深的理解
Pin 的本质不是 bug。
它是:
新调度模型与旧 JVM 结构之间的冲突点。
虚拟线程建立在:
线程身份 ≠ OS 线程
而传统 JVM 的很多结构建立在:
线程身份 = OS 线程
JDK 24 解决了 Monitor 这个历史遗留问题。
但 Native 栈问题属于操作系统边界。
那不是 JVM 可以轻易改变的。
十、总结:Pin 的时代演进
| 版本 | synchronized | Native | 需要避坑 |
|---|---|---|---|
| JDK 21 | 会 Pin | 会 Pin | 避开 synchronized 阻塞 |
| JDK 23 | 会 Pin | 会 Pin | 同上 |
| JDK 24 | 不会 Pin | 会 Pin | 关注 Native |
最后的建议
如果你在 JDK 21 LTS:
-
避免 synchronized + 阻塞
-
优先使用 ReentrantLock
-
开启 JFR 监控
如果你能上 JDK 24:
-
放心使用 synchronized
-
重点关注 Native/FFM
-
理解 Pin 是 JVM 调度边界
一句话总结
Pin 发生在 JVM 无法安全冻结当前执行栈时。
JDK 24 解除了 Monitor 枷锁。
Native 仍然是虚拟线程最后的边界。
更多推荐




所有评论(0)