Java 虚拟线程为何可能被 synchronized 固定
Java 虚拟线程并不是一遇到 synchronized 就一定会被“固定”在 carrier 线程上。这个说法主要来自 JDK 21–23 的实现限制:虚拟线程在 monitor 保护区内遇到某些阻塞操作时,可能无法从 carrier 卸载,长时间、频繁发生就会拖慢整体吞吐。JDK 24 已通过 JEP 491 改善 synchronized 的阻塞处理,但 native 方法和 Foreign Function & Memory API 仍可能造成 pinning。
官方资料:https://docs.oracle.com/en/java/javase/26/core/virtual-threads.html
实际排查时先看运行时版本和 JFR 事件,再决定是否改锁。JDK 24 及以后不需要因为“虚拟线程 + synchronized”这个组合本身就全面替换锁;真正应该优先修的是长时间持锁、锁内阻塞 I/O,以及仍会让虚拟线程无法卸载的 native/foreign 调用。
先把“被固定”理解成 carrier 没有被释放
虚拟线程由 Java 运行时调度,执行时会暂时挂载到一个平台线程上,这个平台线程就是 carrier。普通的阻塞 I/O 通常可以让虚拟线程挂起并释放 carrier,其他虚拟线程随后可以使用这个 carrier。pinning 的关键不是虚拟线程停止工作,而是它在阻塞时继续占着底层平台线程。
synchronized 使用对象 monitor 提供互斥和 happens-before 保证。旧版本中,如果虚拟线程在 synchronized 方法或代码块内执行阻塞操作,或者等待一个竞争激烈的 monitor,carrier 可能被一并占住。一个短小的内存临界区通常不会造成可感知问题;真正危险的是把网络、文件、数据库或外部 SDK 调用包在锁里。

先确认你面对的是哪个 JDK 行为
如果线上服务运行在 JDK 21、22 或 23,历史文章中关于 synchronized pinning 的提醒仍然有参考价值,尤其要关注频繁进入、并且会等待 I/O 的临界区。Oracle 的 JDK 23 指南明确建议避免频繁且长时间的 pinning,并在确有问题时考虑 ReentrantLock。
JDK 24 引入“Synchronize Virtual Threads without Pinning”。Oracle 的 JDK 26 迁移指南说明,JDK 24 会让在 synchronized 方法或语句中阻塞的虚拟线程释放底层平台线程,交给其他虚拟线程使用。因此,升级到 JDK 24+ 后,不能再简单地把所有 synchronized 都判定为 pinning 来源。
| 运行时范围 | 需要重点关注的现象 | 处理判断 |
|---|---|---|
| JDK 21–23 | synchronized 内的长阻塞、竞争 monitor 可能拖住 carrier | 优先缩短临界区,并对高频长阻塞点评估 ReentrantLock |
| JDK 24+ | synchronized 的阻塞可更好地释放 carrier,但 native/foreign 阻塞仍可能 pin | 保留正确的短临界区,使用 JFR 找实际事件 |
| 所有版本 | 锁内做外部调用会放大排队和尾延迟 | 先重构资源边界,不要只换一个锁类名 |
最先改的是锁内的外部调用
下面的旧式写法把远程读取放进了 monitor。即使当前 JDK 能处理一部分 synchronized 阻塞,这种结构仍然让所有竞争者共享一个长临界区:远程服务变慢时,内存状态更新也会跟着排队。
final class ProfileCache {
private final Object lock = new Object();
private Profile profile;
Profile load(String userId) throws IOException {
synchronized (lock) {
// 远程 I/O 的时长不可控,不要让它占着共享 monitor。
Profile loaded = fetchFromService(userId);
profile = loaded;
return loaded;
}
}
private Profile fetchFromService(String userId) throws IOException {
// 示例只表达边界:真实项目里这里可能是 HTTP、JDBC 或文件读取。
return remoteClient.fetch(userId);
}
}
更稳妥的拆法是先在锁外取得数据,再在锁内完成短小的状态合并。若业务要求“只有一个请求负责刷新”,还要额外设计刷新标记或使用更高层的缓存协调机制,不能因为移出 synchronized 就默认消除了重复请求。
final class ProfileCache {
private final Object lock = new Object();
private Profile profile;
Profile load(String userId) throws IOException {
// 先在锁外执行可能长时间阻塞的调用。
Profile loaded = fetchFromService(userId);
synchronized (lock) {
// 锁内只做内存状态更新,保持临界区短小。
profile = loaded;
return profile;
}
}
private Profile fetchFromService(String userId) throws IOException {
// 统一保留底层异常,让调用方决定重试或降级。
return remoteClient.fetch(userId);
}
}
需要可中断或超时时,再考虑 ReentrantLock
ReentrantLock 不是虚拟线程的“自动解 pin 开关”,它的价值在于提供更细的控制:可以尝试加锁、响应中断、设置超时或选择公平策略。只有当这些能力确实解决了你的调度需求,或者在旧 JDK 上定位到长时间、频繁的 synchronized pinning 时,替换才有意义。
final class RefreshGate {
private final ReentrantLock lock = new ReentrantLock();
boolean tryRefresh(Duration timeout) throws InterruptedException {
// 超时让调用方可以放弃等待,而不是无限占用请求资源。
if (!lock.tryLock(timeout.toNanos(), TimeUnit.NANOSECONDS)) {
return false;
}
try {
// 这里应放短小的共享状态操作;不要把远程 I/O 放进锁内。
refreshInMemoryState();
return true;
} finally {
// 无论刷新成功还是抛出异常,都必须释放锁。
lock.unlock();
}
}
}
如果只是保护一个很短的字段读写,synchronized 更直接,也更容易让维护者看出互斥范围。换锁时要同时复核可见性、异常路径和锁对象是否保持一致,不能只做机械替换。

并发上限应该用 Semaphore 表达
很多团队用固定平台线程池限制外部服务并发,迁移到虚拟线程后仍然保留同一个池,结果把“线程资源管理”和“业务并发上限”混在了一起。Oracle 的虚拟线程采用指南建议用 Semaphore 表达受限资源的并发数;数据库连接池本身也已经承担了类似的排队职责。
final class LimitedClient {
private final Semaphore permits = new Semaphore(10);
Response call(Request request) throws InterruptedException {
// 信号量表达的是外部服务最多允许的并发请求数。
permits.acquire();
try {
// 虚拟线程可以在这里等待;不要为每个任务再建一个固定线程池。
return client.send(request);
} finally {
// 无论远程调用成功、失败还是被取消,都归还许可。
permits.release();
}
}
}
用 JFR 找仍然存在的真实 pinning
不要仅凭代码中出现了 synchronized 就判断服务一定被 pinning。JDK 文档提供了 jdk.VirtualThreadPinned 事件,它默认在阻塞超过阈值时记录;JDK 26 虚拟线程文档还说明,native 方法或 foreign function 是当前仍需重点关注的 pinning 来源。
# 启动一次录制,并在 JVM 退出时把 JFR 文件写到指定位置。
java -XX:StartFlightRecording:dumponexit=true,filename=virtual-thread.jfr \
-cp app.jar com.example.Main
# 只打印虚拟线程被固定的事件,定位阻塞操作和调用栈。
jfr print --events jdk.VirtualThreadPinned virtual-thread.jfr
看到事件后,先看 blockingOperation、pinnedReason 和调用栈,再回到代码判断是 monitor、native/foreign 调用,还是其他运行时边界。没有事件时,不要为了迎合旧文章而全面替换 synchronized。
最后用这张表做选择
| 场景 | 优先方案 | 判断依据 |
|---|---|---|
| 短小的内存状态互斥 | synchronized | 语义清楚,临界区短,不涉及外部阻塞 |
| 需要超时、可中断或公平策略 | ReentrantLock | 需要显式控制加锁和释放路径 |
| 限制外部服务或资源并发 | Semaphore | 表达的是许可数量,不是平台线程数量 |
| native/foreign 调用或锁内长 I/O | 重构边界并监控 | 先缩短阻塞范围,再用 JFR 观察事件 |
所以,“Java 虚拟线程为何可能被 synchronized 固定”的准确答案是:旧 JDK 的 monitor 阻塞曾经让 carrier 无法释放,JDK 24 起这部分实现已经改善;现在更应该关注 native/foreign 阻塞和锁内长耗时操作。选择同步工具时,先看临界区、等待策略和资源上限,再看锁的名字。
相关问题
- JDK 24 以后还需要把 synchronized 全部改成 ReentrantLock 吗?
- 虚拟线程里用固定线程池限制并发有什么问题?
- JFR 的 jdk.VirtualThreadPinned 事件应该重点看哪些字段?
- 为什么把网络请求放到 synchronized 外面仍可能产生重复刷新?
Java ModuleDescriptor.Version 怎样比较模块版本
- 上一篇
- Java ModuleDescriptor.Version 怎样比较模块版本
- 下一篇
- Java Character.isEmoji 怎样按 Unicode 码点判断 Emoji
-
- 文章 · java教程 | 37分钟前 | 异常处理 · 内存管理 · java try-with-resources release ByteBuffer ModuleReader ModuleReference 模块资源
- Java ModuleReader 读取模块资源后怎样释放 ByteBuffer
- 294浏览 收藏
-
- 文章 · java教程 | 52分钟前 | 字符串 · Java · API · unicode · java stringbuilder unicode UTF-16 appendCodePoint 码点
- Java StringBuilder appendCodePoint 怎样追加 Unicode 码点
- 388浏览 收藏
-
- 文章 · java教程 | 53分钟前 |
- Java instanceof 模式变量怎样避免重复强制转换
- 108浏览 收藏
-
- 文章 · java教程 | 17小时前 |
- Java Optional.ifPresentOrElse 怎样同时处理有值和空结果
- 242浏览 收藏
-
- 文章 · java教程 | 17小时前 | 并发 · Java · java arena MemorySegment FFM API
- Java Arena.ofConfined 与 ofShared 如何选择
- 335浏览 收藏
-
- 文章 · java教程 | 17小时前 | java Event JFR shouldCommit 事件过滤
- Java JFR Event 怎样只记录满足条件的事件
- 412浏览 收藏
-
- 文章 · java教程 | 17小时前 | 虚拟线程 · 性能边界 · java 并发 threadlocal 资源释放 ScopedValue Virtual Threads
- Java virtual threads 与 ThreadLocal 资源绑定的清理边界
- 339浏览 收藏
-
- 文章 · java教程 | 17小时前 | java websocket 重连 httpclient 消息顺序
- Java HttpClient WebSocket 怎样处理重连后的消息顺序
- 146浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 1029次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 1030次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1173次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1133次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 973次使用
-
- Java 时间格式兼容多个输入格式的方法
- 2026-10-18 501浏览
-
- Java try-with-resources 多个资源关闭顺序是什么
- 2026-09-10 501浏览
-
- 矩阵主副对角线快速定位技巧
- 2026-05-31 501浏览
-
- Java多态优化流程代码与行为分发改进
- 2026-05-26 501浏览
-
- JVM 类元数据双亲委派链表深度解析
- 2026-05-21 501浏览

