资讯动态

synchronized 会 Pin 虚拟线程?一文讲透底层原理与 JDK 24 的终极解决方案

发布时间:2026/10/15 11:08:05 来源:尧图企业网站定制
一、前言Java 21 正式引入虚拟线程后很多开发者迫不及待地将其应用到生产环境。然而不少人很快发现虚拟线程的性能远低于预期甚至出现了服务卡死的情况。罪魁祸首之一就是Pinning固定——当虚拟线程在synchronized块中阻塞时它会被“钉”在载体线程上导致载体线程无法去执行其他虚拟线程。核心结论先行在 JDK 21 ~ JDK 23 中synchronized会导致虚拟线程 PinningJDK 24 通过 JEP 491 彻底解决了这个问题。本文将从底层原理到 JDK 24 的解决方案带你完整理解这个问题的来龙去脉。二、虚拟线程基础回顾2.1 虚拟线程的挂载与卸载虚拟线程Virtual Thread是 JDK 而非操作系统提供的轻量级线程。它的核心优势在于当虚拟线程发生阻塞时JVM 可以将其从载体线程Carrier Thread上“卸载”让载体线程去执行其他虚拟线程。虚拟线程调度模型挂载挂载等待虚拟线程 A载体线程 1平台线程虚拟线程 B载体线程 2平台线程虚拟线程 C调度队列虚拟线程底层复用载体线程来执行任务。当虚拟线程发生阻塞时JVM 会将虚拟线程从载体线程上“卸载”yield——中断载体线程把线程栈中涉及虚拟线程的部分复制到堆内存中保存载体线程再去执行其他任务。IO 就绪后JVM 重新装载虚拟线程继续执行。2.2 什么是 PinningPinning 指的是虚拟线程在阻塞时无法从载体线程上卸载的状态。一旦发生 Pinning载体线程就被这个虚拟线程“独占”了无法去执行其他虚拟线程——虚拟线程的核心优势瞬间丧失。三、synchronized 为什么会导致 Pinning3.1 根本原因锁的所有者是载体线程在 Java 24 之前当虚拟线程进入synchronized方法或代码块时JVM 会将锁Monitor的所有者记录为底层的载体线程Carrier Thread而不是虚拟线程本身。这带来了一个根本性的矛盾如果 JVM 允许虚拟线程在持有锁时被卸载那么载体线程就可能被调度去执行其他虚拟线程但此时锁的所有者信息仍然指向这个载体线程会导致锁的所有权信息混乱。为了避免这种混乱JVM 采取了一个简单粗暴的策略只要虚拟线程在 synchronized 块内就禁止将其从载体线程上卸载。3.2 更严重的情况进入 synchronized 失败时也会 Pinning根据 JEP 425 的原始描述虚拟线程在“执行 synchronized 块内的代码”时会被 Pinning。但实际上当虚拟线程尝试进入 synchronized 块但获取锁失败时它就已经被 Pinning 了。这意味着synchronized(object){// 虚拟线程在这段代码执行期间被 Pinning// 即使只是在等待获取锁就已经被 Pinning 了}这个行为在早期引发了广泛的讨论——有开发者发现MongoDB Java Driver 使用synchronized块后应用程序因为载体线程被“消耗”而挂起。Alan BatemanProject Loom 核心开发者确认这是预期行为“当前限制是虚拟线程在阻塞等待 monitor 时无法被卸载因为 monitor 被另一个线程持有。”3.3 为什么 Pinning 会导致程序卡死阿里云开发者社区的一篇文章深入分析了 synchronized Pinning 导致死锁的场景虚拟线程 A 成功获取 object monitor但需要等待某种资源因此 Pinning其余许多虚拟线程在等待 object monitor占用了剩余的载体线程因此 Pinning能够释放某种资源的虚拟线程任务没有载体线程可以执行无法唤醒虚拟线程 A程序完全停止。获取锁成功等待资源等待锁等待锁无载体线程可用无法唤醒 A虚拟线程 A持有 object monitor被 Pinning占用载体线程 1虚拟线程 B被 Pinning占用载体线程 2虚拟线程 C被 Pinning占用载体线程 3虚拟线程 D可以释放资源无法执行根本原因在于ObjectMonitor::enter的相关逻辑是用 C/C 编写的其调用栈是 C native frame含有 native frame 的虚拟线程无法进行上下文切换。3.4 完整的 Pinning 场景综合来看在 Java 21~23 中以下两种场景会导致虚拟线程 PinningPinning 场景说明执行 synchronized 块/方法内的代码无论是否发生阻塞只要在 synchronized 内就被 Pinning执行 native 方法或外部函数JNI 调用、Foreign Function Memory API四、诊断 Pinning 问题4.1 JFR 事件jdk.VirtualThreadPinnedJDK 提供了 JFRJava Flight Recorder事件来诊断 Pinning 问题。jdk.VirtualThreadPinned事件表示虚拟线程被 Pinning 且载体线程未被释放超过阈值时间默认 20ms。使用方式# 启动应用时开启 JFRjava-XX:StartFlightRecording:filenamepinning.jfr,duration60s MyApp# 或使用 jcmd 动态开启jcmdpidJFR.startnamepinningduration60sfilenamepinning.jfr4.2 分析 JFR 记录jfr print--eventsjdk.VirtualThreadPinned pinning.jfr输出示例jdk.VirtualThreadPinned { startTime 10:31:19.654 duration 25 ms eventThread virtual-thread-1 (virtual) stackTrace [ java.lang.Object.wait(Object.java) com.example.MyService.process(MyService.java:42) ... ] }注意jdk.VirtualThreadPinned事件只在运行时尝试 park 一个被 Pinning 的线程时才会触发。例如Thread.sleep()调用会触发该事件但仅仅是进入 synchronized 块不会触发。4.3 传统的 jdk.tracePinnedThreads在 JDK 21~23 中可以通过系统属性输出 Pinning 时的线程栈java-Djdk.tracePinnedThreadsfull MyApp不过在 JDK 24 的 EA 构建中这个属性已经不再打印任何内容建议使用 JFR 事件替代。五、JDK 24 的终极方案JEP 4915.1 概述JEP 491: Synchronize Virtual Threads without Pinning是 JDK 24 中最重要的虚拟线程改进之一。它的目标是让虚拟线程在 synchronized 块中阻塞时能够释放其底层平台线程供其他虚拟线程使用从而消除几乎所有虚拟线程被 Pinning 的情况。5.2 核心实现原理JEP 491 的核心思路是让锁的所有权不再依赖于具体的载体线程。具体来说做了两件事第一锁记录的转移。当虚拟线程被卸载冻结时JVM 会将其持有的锁信息从载体线程的 LockStack 中复制出来保存到虚拟线程的 stackChunk 中当虚拟线程被重新挂载解冻时再将锁信息恢复回去。第二锁所有者的记录方式变更。对于膨胀锁Inflated MonitorJVM 不再记录持有锁的载体线程JavaThread*而是直接记录拥有该锁的java.lang.Thread的 IDtid。这样锁的所有者就与具体的载体线程解耦了。JDK 24: 锁所有者 虚拟线程synchronized持有虚拟线程可卸载虚拟线程 A载体线程 1Monitorowner 虚拟线程 A✅ 载体线程释放JDK 21~23: 锁所有者 载体线程synchronized持有虚拟线程无法卸载虚拟线程 A载体线程 1Monitorowner 载体线程 1❌ Pinning5.3 改动的范围JEP 491 的实现覆盖了以下场景允许虚拟线程在持有 monitor的情况下被卸载允许虚拟线程在阻塞等待获取 monitor时被卸载允许虚拟线程在Object.wait()及定时等待变体中阻塞时被卸载该修复覆盖了所有 4 个实现 continuations 的平台x64、aarch64、riscv 和 ppc。5.4 效果验证社区提供了一个直观的性能对比 Demo。测试代码启动 5000 个虚拟线程每个线程先做 CPU 计算然后获取一个独立的锁并在锁内阻塞 5ms虚拟线程调度器并行度限制为 1JDK 版本总执行时间JDK 2131.791 秒JDK 240.454 秒性能提升接近70 倍。 这个 Demo 的关键设计是每个任务使用不同的锁对象。如果使用共享锁synchronized 块本身就是串行执行的无法体现 Pinning 带来的差异。六、JDK 21~23 的应对方案ReentrantLock在 JDK 24 发布之前官方推荐的解决方案是使用ReentrantLock替代synchronized。ReentrantLock属于java.util.concurrent.locks包其锁机制与 Monitor 不同不会导致虚拟线程 Pinning。6.1 代码对比// ❌ JDK 21~23 中会 Pinning 的写法publicsynchronizedvoidwrongMethod(){try{Thread.sleep(1000);// 阻塞操作导致 Pinning}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}// ✅ 使用 ReentrantLock不会 PinningprivatefinalReentrantLocklocknewReentrantLock();publicvoidrightMethod(){lock.lock();try{Thread.sleep(1000);// 虚拟线程可以被卸载}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{lock.unlock();}}6.2 注意事项短时间持有的锁不需要替换JEP 425 原文指出只在锁内执行内存操作且不涉及长时间阻塞的场景不需要替换只在锁保护 IO 操作时才需要替换MongoDB 的案例中开发者最终只需要替换保护 socket IO 的锁而不是所有锁6.3 JDK 24 的变化从 JDK 24 开始synchronized已经不再需要替换为ReentrantLock了。如果你的项目仍在使用 JDK 21~23替换方案依然有效但如果你已经升级到 JDK 24直接用synchronized即可。⚠️ 社区的提醒不要再拿 JDK 21 的“口诀”去改 JDK 25 的代码了。JDK 24 已经修复了 synchronized 的 Pinning 问题盲目替换反而增加代码复杂度。七、其他 Pinning 场景7.1 Native 代码调用JEP 491 只解决了synchronized导致的 Pinning。调用 native 代码JNI 或 Foreign Function Memory API仍会导致 Pinning这在所有 Java 版本中都存在。原因是虚拟线程的栈存储在 Java 堆中可以在内存中移动。但 native 代码的栈帧难以移动因为它们可能包含指向栈本身的指针而 Java 无法感知这些指针。synchronizedJNI / FFM纯 Java 阻塞虚拟线程调用类型?JDK 24 已解决仍然 Pinning正常卸载7.2 类加载与初始化类加载和初始化过程中涉及 VM 内部的 C/C 代码如果此时遇到需要切出的事件也会导致 Pinning。这类问题比较隐蔽需要通过 JFR 事件来定位。八、总结对比项JDK 21~23JDK 24synchronized 是否 Pinning✅ 是❌ 否等待 monitor 是否 Pinning✅ 是❌ 否Object.wait() 是否 Pinning✅ 是❌ 否native 代码是否 Pinning✅ 是✅ 是未改变推荐方案用 ReentrantLock 替代可直接使用 synchronized核心要点回顾Pinning 的本质是虚拟线程在阻塞时无法从载体线程卸载导致载体线程被“浪费”JDK 21~23 的问题在于 JVM 将 synchronized 锁的所有者记录为载体线程为了避免锁所有权混乱禁止虚拟线程卸载JDK 24 的 JEP 491通过将锁的所有者从载体线程改为虚拟线程本身实现了锁所有权与载体线程的解耦从根本上解决了 Pinning 问题Native 代码的 Pinning在所有版本中都存在目前尚无解决方案诊断工具使用 JFR 的jdk.VirtualThreadPinned事件来定位 Pinning 问题如果你的项目还在 JDK 21~23建议对长时间持有且涉及 IO 的synchronized块替换为ReentrantLock通过 JFR 监控 Pinning 事件定位性能瓶颈如果条件允许尽快升级到 JDK 24从根源上解决问题

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑