如果你正在使用 JDK 21 的虚拟线程,那么 Pinning 是你绕不开的一课。
如果你准备升级到 JDK 24,那么这篇文章会告诉你——游戏规则已经变了。

一、虚拟线程真正的本质:Continuation + 可卸载栈

虚拟线程(Virtual Thread, VT)之所以“便宜”,不是因为它更快。

而是因为:

它可以在阻塞时从平台线程(Carrier Thread)上卸载(Unmount)。

底层机制依赖:

Continuation.freeze() Continuation.mount()

当 VT 遇到阻塞点:

  1. JVM 尝试冻结当前调用栈(拷贝 stack chunk 到堆)

  2. 将 VT 与 Carrier 解绑定

  3. 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); }

发生的底层过程是:

  1. Monitor.owner 绑定当前 Carrier

  2. VT 遇到阻塞

  3. JVM 尝试 freeze

  4. 检查发现持有 Monitor

  5. 无法保证锁语义跨 Carrier 正确

  6. 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 仍然是虚拟线程最后的边界。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐