news 2026/10/15 11:08:02

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

作者头像

张小明

前端开发工程师

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

一、前言

Java 21 正式引入虚拟线程后,很多开发者迫不及待地将其应用到生产环境。然而,不少人很快发现:虚拟线程的性能远低于预期,甚至出现了服务卡死的情况。

罪魁祸首之一就是Pinning(固定)——当虚拟线程在synchronized块中阻塞时,它会被“钉”在载体线程上,导致载体线程无法去执行其他虚拟线程。

📌核心结论先行:在 JDK 21 ~ JDK 23 中,synchronized会导致虚拟线程 Pinning;JDK 24 通过 JEP 491 彻底解决了这个问题。本文将从底层原理到 JDK 24 的解决方案,带你完整理解这个问题的来龙去脉。

二、虚拟线程基础回顾

2.1 虚拟线程的挂载与卸载

虚拟线程(Virtual Thread)是 JDK 而非操作系统提供的轻量级线程。它的核心优势在于:当虚拟线程发生阻塞时,JVM 可以将其从载体线程(Carrier Thread)上“卸载”,让载体线程去执行其他虚拟线程。

虚拟线程调度模型

挂载

挂载

等待

虚拟线程 A

载体线程 1
平台线程

虚拟线程 B

载体线程 2
平台线程

虚拟线程 C

调度队列

虚拟线程底层复用载体线程来执行任务。当虚拟线程发生阻塞时,JVM 会将虚拟线程从载体线程上“卸载”(yield)——中断载体线程,把线程栈中涉及虚拟线程的部分复制到堆内存中保存,载体线程再去执行其他任务。IO 就绪后,JVM 重新装载虚拟线程继续执行。

2.2 什么是 Pinning?

Pinning 指的是虚拟线程在阻塞时无法从载体线程上卸载的状态。一旦发生 Pinning,载体线程就被这个虚拟线程“独占”了,无法去执行其他虚拟线程——虚拟线程的核心优势瞬间丧失。

三、synchronized 为什么会导致 Pinning?

3.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 Bateman(Project Loom 核心开发者)确认这是预期行为:“当前限制是,虚拟线程在阻塞等待 monitor 时无法被卸载,因为 monitor 被另一个线程持有。”

3.3 为什么 Pinning 会导致程序卡死?

阿里云开发者社区的一篇文章深入分析了 synchronized Pinning 导致死锁的场景:

  1. 虚拟线程 A 成功获取 object monitor,但需要等待某种资源,因此 Pinning;
  2. 其余许多虚拟线程在等待 object monitor,占用了剩余的载体线程,因此 Pinning;
  3. 能够释放某种资源的虚拟线程任务没有载体线程可以执行,无法唤醒虚拟线程 A;
  4. 程序完全停止。

获取锁成功

等待资源

等待锁

等待锁

无载体线程可用

无法唤醒 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 中,以下两种场景会导致虚拟线程 Pinning:

Pinning 场景说明
执行 synchronized 块/方法内的代码无论是否发生阻塞,只要在 synchronized 内就被 Pinning
执行 native 方法或外部函数JNI 调用、Foreign Function & Memory API

四、诊断 Pinning 问题

4.1 JFR 事件:jdk.VirtualThreadPinned

JDK 提供了 JFR(Java Flight Recorder)事件来诊断 Pinning 问题。jdk.VirtualThreadPinned事件表示虚拟线程被 Pinning 且载体线程未被释放超过阈值时间(默认 20ms)。

使用方式:

# 启动应用时开启 JFRjava-XX:StartFlightRecording:filename=pinning.jfr,duration=60s MyApp# 或使用 jcmd 动态开启jcmd<pid>JFR.startname=pinningduration=60sfilename=pinning.jfr

4.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.tracePinnedThreads=full MyApp

不过在 JDK 24 的 EA 构建中,这个属性已经不再打印任何内容,建议使用 JFR 事件替代。

五、JDK 24 的终极方案:JEP 491

5.1 概述

JEP 491: Synchronize Virtual Threads without Pinning是 JDK 24 中最重要的虚拟线程改进之一。它的目标是:让虚拟线程在 synchronized 块中阻塞时,能够释放其底层平台线程供其他虚拟线程使用,从而消除几乎所有虚拟线程被 Pinning 的情况。

5.2 核心实现原理

JEP 491 的核心思路是让锁的所有权不再依赖于具体的载体线程。

具体来说,做了两件事:

第一,锁记录的转移。当虚拟线程被卸载(冻结)时,JVM 会将其持有的锁信息从载体线程的 LockStack 中复制出来,保存到虚拟线程的 stackChunk 中;当虚拟线程被重新挂载(解冻)时,再将锁信息恢复回去。

第二,锁所有者的记录方式变更。对于膨胀锁(Inflated Monitor),JVM 不再记录持有锁的载体线程(JavaThread*),而是直接记录拥有该锁的java.lang.Thread的 ID(tid)。这样锁的所有者就与具体的载体线程解耦了。

JDK 24: 锁所有者 = 虚拟线程

synchronized

持有

虚拟线程可卸载

虚拟线程 A

载体线程 1

Monitor
owner = 虚拟线程 A

✅ 载体线程释放

JDK 21~23: 锁所有者 = 载体线程

synchronized

持有

虚拟线程无法卸载

虚拟线程 A

载体线程 1

Monitor
owner = 载体线程 1

❌ Pinning

5.3 改动的范围

JEP 491 的实现覆盖了以下场景:

  1. 允许虚拟线程在持有 monitor的情况下被卸载
  2. 允许虚拟线程在阻塞等待获取 monitor时被卸载
  3. 允许虚拟线程在Object.wait()及定时等待变体中阻塞时被卸载

该修复覆盖了所有 4 个实现 continuations 的平台:x64、aarch64、riscv 和 ppc。

5.4 效果验证

社区提供了一个直观的性能对比 Demo。测试代码启动 5000 个虚拟线程,每个线程先做 CPU 计算,然后获取一个独立的锁并在锁内阻塞 5ms,虚拟线程调度器并行度限制为 1:

JDK 版本总执行时间
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,不会 PinningprivatefinalReentrantLocklock=newReentrantLock();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 无法感知这些指针。

synchronized

JNI / FFM

纯 Java 阻塞

虚拟线程

调用类型?

JDK 24+ 已解决

仍然 Pinning

正常卸载

7.2 类加载与初始化

类加载和初始化过程中涉及 VM 内部的 C/C++ 代码,如果此时遇到需要切出的事件,也会导致 Pinning。这类问题比较隐蔽,需要通过 JFR 事件来定位。

八、总结

对比项JDK 21~23JDK 24+
synchronized 是否 Pinning✅ 是❌ 否
等待 monitor 是否 Pinning✅ 是❌ 否
Object.wait() 是否 Pinning✅ 是❌ 否
native 代码是否 Pinning✅ 是✅ 是(未改变)
推荐方案用 ReentrantLock 替代可直接使用 synchronized

核心要点回顾:

  1. Pinning 的本质是虚拟线程在阻塞时无法从载体线程卸载,导致载体线程被“浪费”
  2. JDK 21~23 的问题在于 JVM 将 synchronized 锁的所有者记录为载体线程,为了避免锁所有权混乱,禁止虚拟线程卸载
  3. JDK 24 的 JEP 491通过将锁的所有者从载体线程改为虚拟线程本身,实现了锁所有权与载体线程的解耦,从根本上解决了 Pinning 问题
  4. Native 代码的 Pinning在所有版本中都存在,目前尚无解决方案
  5. 诊断工具:使用 JFR 的jdk.VirtualThreadPinned事件来定位 Pinning 问题

如果你的项目还在 JDK 21~23,建议:

  • 对长时间持有且涉及 IO 的synchronized块,替换为ReentrantLock
  • 通过 JFR 监控 Pinning 事件,定位性能瓶颈
  • 如果条件允许,尽快升级到 JDK 24+,从根源上解决问题
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:07:36

卷积神经网络推理内存三笔账:权重、特征图与临时缓冲

1. 一个让很多人困惑的现象&#xff1a;权重才几兆&#xff0c;跑起来却吃掉几个G先把场景摆出来。你训练好一个卷积网络&#xff0c;权重文件导出成 FP32 格式&#xff0c;一看大小&#xff0c;比如 3.5 MB、12 MB&#xff0c;甚至更小。你心想这玩意儿这么轻&#xff0c;部署…

作者头像 李华
网站建设 2026/10/15 11:07:35

无图化不是无空间数据:自动驾驶底层逻辑与具身智能实践

1. 被误读的“无图化”&#xff1a;一场关于自动驾驶底层逻辑的认知纠偏 “无图化”这个词&#xff0c;最近两年在自动驾驶圈子里被提及的频率极高。但凡有新车发布、技术沟通会&#xff0c;或者哪家供应商拿到了新一轮融资&#xff0c;几乎都会把“无图化方案”当作核心卖点来…

作者头像 李华
网站建设 2026/10/15 11:07:08

Claude实战:用TaoToken统一Key打通/goal插件,条件判断终于不用手写

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 11:05:26

日系 PLC 数据怎么采?DXPServer 的驱动与配置复用

DXPServer&#xff08;DeviceXPlorer OPC Server&#xff09;在日系设备项目中可以承接三类工作&#xff1a;通过对应驱动访问设备数据&#xff0c;复用 GX Works3 的工程标签&#xff0c;以及导入既有 MX OPC Server 的部分配置。 典型数据链路可以理解为&#xff1a;支持范围…

作者头像 李华
网站建设 2026/10/15 11:03:50

Python异步编程入门:asyncio协程与事件循环实战指南

如果你用Python写过爬虫&#xff0c;大概率经历过这种尴尬&#xff1a;请求发出去之后&#xff0c;程序眼巴巴等着服务器返回&#xff0c;同一时刻CPU几乎闲置&#xff0c;可整个脚本的耗时却全被网络延迟拖垮。几千个URL要抓&#xff0c;每个请求两三百毫秒&#xff0c;同步写…

作者头像 李华
网站建设 2026/10/15 11:03:36

python正则表达式实例代码

前言 正则表达式&#xff08;regular expression&#xff0c;简称 regex&#xff09;是处理文本的通用工具&#xff1a;在一堆字符里找出满足某种形状的片段&#xff0c;或者把符合形状的片段替换成别的内容。Python 通过标准库 re 模块提供这套能力&#xff0c;不需要安装任何…

作者头像 李华