Java virtual thread 使用 synchronized 后为什么仍会阻塞平台线程
很多人第一次把 Java virtual thread 接到旧的同步代码上,会看到“平台线程也被占住了”的现象,于是把原因简单归结为 synchronized。这个判断要先加上 JDK 版本:JDK 21–23 中,虚拟线程在同步块里执行阻塞操作,确实可能固定 carrier;从 JDK 24 开始,JEP 491 已让虚拟线程在常规监视器等待和持锁阻塞时释放底层平台线程。若 JDK 24+ 仍出现平台线程长时间阻塞,优先检查调用者是不是平台线程、是否进入 native/Foreign Function,或临界区里根本不是“等待”而是持续计算。
结论先说:synchronized仍然会造成互斥和锁等待,但在 JDK 24+ 不应再把普通监视器竞争自动等同于 virtual thread pinning。先看 JFR 的阻塞原因和 JDK 版本,再决定是否缩小临界区或换成ReentrantLock。
- 锁等待是应用层语义,线程固定是运行时调度问题,两者不是同一个指标。
- JDK 21–23 要警惕 synchronized 包住 I/O;JDK 24+ 普通 monitor 已支持释放 carrier。
- JFR 的
jdk.VirtualThreadPinned和jcmd Thread.dump_to_file比线程名更可靠。
先分清“锁等待”和“线程固定”
synchronized 做的第一件事是获取对象监视器。已有线程持锁时,后来者要等待;这说明共享资源有竞争,并不自动说明载体平台线程被永久占住。虚拟线程通常可以在阻塞 I/O 时从 carrier 卸载,让同一个 carrier 去运行别的虚拟线程,但某些运行时边界会阻止卸载。
可以把现象拆成四个问题:谁在等 monitor,谁持有 monitor;等待发生在虚拟线程还是平台线程;等待期间是否进入阻塞 I/O;JFR 是否记录了 pinned 事件。下面的关系图只表达静态边界,不代表执行时序。

JDK 版本会改变 synchronized 的判断
旧版本的典型风险是把慢 I/O 放进同步块:
public synchronized String loadProfile() throws IOException {
// 旧版 JDK 中,这里的阻塞 I/O 可能固定虚拟线程的 carrier
return httpClient.send(request, BodyHandlers.ofString()).body();
}
在 JDK 21–23,频繁且长时间的这种写法会减少可用 carrier,吞吐量随并发上升而变差。短暂的内存操作或只在启动阶段使用的 synchronized 通常不值得为了虚拟线程强行改写。
JDK 24 的变化是“同步而不固定”:虚拟线程可以在等待 monitor 时卸载,也可以在持有 monitor 的代码遇到可卸载的阻塞时释放平台线程。它没有取消 synchronized 的互斥、可见性或 happens-before 语义,也不代表锁内代码会变快。平台线程本身仍然不能像虚拟线程一样卸载;如果调用者就是平台线程,它等待 monitor 时当然会处于阻塞状态。
| 看到的现象 | 更可能的含义 | 先查什么 |
|---|---|---|
| 虚拟线程在 monitor 上等待 | 锁竞争,JDK 24+ 不必然是 pinning | 持锁线程与临界区长度 |
| carrier 长时间不释放 | 旧 JDK、native/foreign function 或不可卸载边界 | JFR VirtualThreadPinned |
| 平台线程处于 BLOCKED | 平台线程自己在等 monitor,或被锁内工作占用 | 线程转储与调用栈 |
用 JFR 和 jcmd 定位真实阻塞点
不要只根据线程名里的 ForkJoinPool 或 carrier 下结论。Oracle 文档给出的 JFR 事件 jdk.VirtualThreadPinned 默认在超过 20ms 时记录,并且会带出阻塞操作与原因。可以在复现窗口采集一段 recording,再筛选相关事件:
# 采集结束后,只打印虚拟线程相关事件 jfr print --events jdk.VirtualThreadPinned,jdk.VirtualThreadSubmitFailed recording.jfr # 导出线程转储,保留平台线程与虚拟线程的调用栈 jcmdThread.dump_to_file -format=json threads.json
若事件原因指向 native 或 foreign function,换锁通常不是根治;应先把这段调用移出高频临界区,或评估它是否必须在虚拟线程中执行。若只有大量 monitor 竞争而没有 pinned 事件,问题更接近锁粒度、持锁时间或平台线程调用者。
按 JDK 版本和阻塞类型选择修复
修复顺序应当是“先证据,后替换”。对保护内存状态的短临界区,保留 synchronized 往往更清晰;对可能做慢 I/O 的高频路径,先缩小锁区:
public String loadProfile() throws IOException {
String requestId;
synchronized (stateLock) {
// 只在锁内读取共享状态,不把网络调用带进临界区
requestId = currentRequestId;
}
// 网络等待发生在锁外,减少其他任务的排队时间
return httpClient.send(buildRequest(requestId), BodyHandlers.ofString()).body();
}
如果业务确实需要可中断、可超时或更灵活的锁策略,再考虑 ReentrantLock,并用 try/finally 保证释放:
lock.lock();
try {
// 临界区只保留必须互斥的内存操作
updateCacheEntry();
} finally {
// 即使业务抛出异常,也必须归还锁
lock.unlock();
}
不要为了追求“虚拟线程化”而给所有 synchronized 加一层锁,也不要用固定平台线程池假装限制外部服务并发;并发上限应由明确的信号量或资源池承担。

相关问题
JDK 24+ 还需要把 synchronized 全部换成 ReentrantLock 吗?
不需要。先用 JFR 证明存在频繁、长时间的 pinning,再针对慢 I/O 或不可控调用缩小临界区;短内存操作保留 synchronized 通常更简单。
为什么虚拟线程很多,但平台线程数量没有一起增加?
虚拟线程由 Java runtime 调度并复用少量 carrier。它的价值是提高等待型任务的并发吞吐,不是让每个任务都拥有一个新的 OS 线程。
看到 BLOCKED 就说明发生了 pinning 吗?
不是。BLOCKED 可能只是某个线程等待 monitor;pinning 要看虚拟线程是否在不可卸载边界中占住 carrier,JFR 的 jdk.VirtualThreadPinned 更适合做判断。
参考:Oracle Java SE 25 Virtual Threads、JDK 24 Significant Changes、OpenJDK JEP 444。
Go image/draw 怎么把缩略图裁成固定比例
- 上一篇
- Go image/draw 怎么把缩略图裁成固定比例
- 下一篇
- Go 编译报 undefined: C 时怎么确认是不是漏了 cgo 文件
-
- 文章 · java教程 | 1小时前 | 并发编程 · Java教程 · 性能选型 · java LongAdder AtomicLong 高并发计数
- Java LongAdder 计数高并发时为什么比 AtomicLong 更适合
- 243浏览 收藏
-
- 文章 · java教程 | 2小时前 | Java · 性能 · nio · 文件映射 · java nio FileChannel MappedByteBuffer FileChannel.map
- Java NIO FileChannel 映射文件过大时怎么控制内存占用
- 391浏览 收藏
-
- 文章 · java教程 | 4小时前 | Java · Stream · nio · 文件遍历 · java path Stream Files.walk Files.find
- Java Files.walk 的深度参数为什么不影响当前目录
- 178浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- Java Pattern 匹配 Unicode 字符时怎么选择 UNICODE_CHARACTER_CLASS
- 347浏览 收藏
-
- 文章 · java教程 | 6小时前 | Java教程 · 类型系统 · 编译排错 · sealed interface · java 编译错误 sealed interface permits 密封接口
- Java sealed interface 扩展失败时怎么检查 permits 列表
- 128浏览 收藏
-
- 文章 · java教程 | 8小时前 | Java · divide · BigDecimal · ArithmeticException ·
- Java BigDecimal 除法出现 ArithmeticException 时怎么选精度
- 266浏览 收藏
-
- 文章 · java教程 | 10小时前 |
- Java HttpClient 上传文件时怎么构造 multipart 请求体
- 225浏览 收藏
-
- 文章 · java教程 | 11小时前 |
- Java CompletableFuture allOf 异常时怎么找到具体失败任务
- 429浏览 收藏
-
- 文章 · java教程 | 13小时前 |
- Java Optional 链式处理后怎么区分空值和异常
- 242浏览 收藏
-
- 文章 · java教程 | 14小时前 |
- Java Stream groupingBy 后怎么统计每组的最大时间记录
- 426浏览 收藏
-
- 文章 · java教程 | 16小时前 |
- Java HttpClient 收到 404 时为什么 future 仍然正常完成
- 483浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 34次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 189次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 127次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 50次使用
-
- Generrated
- Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
- 35次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go保证并发安全底层实现详解
- 2023-02-24 417浏览
-
- Go语言开发保证并发安全实例详解
- 2023-01-07 328浏览
-
- Golang 手写一个简单的并发任务 manager
- 2022-12-23 367浏览
-
- Go Java 算法之字符串解码示例详解
- 2023-01-07 479浏览

