当前位置:首页 > 文章列表 > 文章 > java教程 > Java 21 虚拟线程为什么越开越慢:从 synchronized pinning 到 JFR 定位

Java 21 虚拟线程为什么越开越慢:从 synchronized pinning 到 JFR 定位

来源:17golang原创 2026-08-09 02:35:24 0浏览 收藏

压测 Java 21 写的HTTP服务时,请求从平台线程切到虚拟线程运行,初期压测曲线表现正常;并发从500逐步拉到5000,吞吐反而开始下跌,此时CPU没跑满,总线程数也没出现异常暴涨。统计慢请求的共性,会发现这些请求全都卡在同一个 synchronized 方法里,等待外部I/O返回。

实践要点
  • Java 21 中,虚拟线程在 synchronized 保护区内执行阻塞I/O操作,可能直接pin住承载它的平台线程。
  • 先用 jdk.VirtualThreadPinned-Djdk.tracePinnedThreads=full 定位到具体问题调用栈,再评估要不要调整锁逻辑。
  • 代码量小、触发频率低的内存临界区完全不用动;调用频繁且内部可能触发网络、磁盘等待的临界区,优先评估 ReentrantLock 改造方案。
  • JDK 24 通过 JEP 491 优化了 synchronized 对应的虚拟线程运行行为,但native方法或者外部函数调用导致的pinning问题,仍然需要单独排查。

吞吐下降不是因为虚拟线程“开太多”

线上排查最容易踩的误区,就是看到虚拟线程数量很高,先直接把并发上限调小。这么做只是临时降低了服务压力,根本不能证明虚拟线程本身存在缺陷。Java 21 虚拟线程的运行机制是把轻量虚拟线程映射到操作系统平台线程上跑;遇到支持自动挂起的阻塞操作时,虚拟线程会主动让出绑定的平台线程,调度器可以把其他待运行的虚拟线程调度到这个空出来的平台线程上继续执行。

但在 synchronized 方法或者同步代码块内部发生长时间阻塞时,虚拟线程没法正常释放绑定的平台线程,这个被占用的承载线程就会彻底卡死。这就是常说的pinning现象。此时从外部看平台线程池资源根本没耗尽,但真正能用来调度虚拟线程的可用carrier线程,全都被卡在等远端接口响应的位置。

时间线:一个锁把全链路请求拖进了等待链

这次问题的最小复现代码并不复杂,就是一段给本地缓存刷新加锁的逻辑:

final class ProfileCache {
    private final HttpClient client = HttpClient.newHttpClient();

    synchronized String refresh(String userId) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://profile.example.test/users/" + userId))
            .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }
}

请求进入 refresh 之后,整个等待链路非常清晰:线程先拿到对象监视器,接下来触发了网络I/O等待;后面进来的其他虚拟线程连监视器入口都挤不进去。反映到线上监控里,往往会出现接口平均耗时看起来正常,但P99延迟跟着并发量上涨突然陡升。

Java 21 虚拟线程在 synchronized 内等待网络响应,平台承载线程被 pin 住并形成等待链的时间线
观察到的现象更可能的解释优先排查方向
CPU占用不高,P99延迟持续上涨carrier线程被阻塞,或者锁竞争持续加剧JFR 里的 VirtualThreadPinned 事件
总线程数很多,但吞吐完全不上涨大量请求同时卡在同一个临界区里锁持有总时长、对应操作的调用栈
偶发长尾请求,重试之后直接恢复正常慢I/O把锁的持有时间成倍放大外部依赖耗时、锁范围内的所有操作

触发条件:synchronized 作用域里同时出现锁占用和长等待

完全没必要把所有 synchronized 都当成性能问题来改。只要临界区里只做几个简单的内存字段更新,执行速度极快,pinning持续的时间短到不可能形成任何明显的性能影响。真正会出问题的是两个条件同时叠加:对应方法的调用频率非常高,并且锁的保护范围里包了网络请求、文件读写、数据库操作或者native方法调用。

我们可以先把锁范围内的代码按操作类型拆分。Map 查找、状态位更新和计数器递增这类操作属于短耗时操作;HTTP 请求、Files.readString、JDBC 查询和第三方外部SDK调用都要归到可能产生长等待的操作里。先别急着改JVM配置,先确认阻塞操作真的发生在持有锁的时间段里。

JFR 怎么把 pinning 从主观猜测变成实锤证据

启动测试服务的时候先开一段短时录制,只要复现30秒左右的压测流量就足够拿到足够的样本:

java \
  -XX:StartFlightRecording=filename=vt-pinning.jfr,dumponexit=true \
  -Djdk.tracePinnedThreads=full \
  -jar profile-service.jar

录制结束后过滤出所有和虚拟线程相关的事件:

jfr print --events jdk.VirtualThreadPinned,jdk.VirtualThreadSubmitFailed vt-pinning.jfr

jdk.VirtualThreadPinned 默认筛选出耗时超过20ms的pinning事件。输出结果里优先看虚拟线程的调用栈、对应的承载线程编号,还有被阻塞的具体操作;不要抓到单个异常事件就下结论,还要对应上同时间段的外部依赖耗时指标和锁竞争监控数据交叉验证。

如果暂时没法生成完整JFR录制文件,开启指定参数之后,-Djdk.tracePinnedThreads=full 会在pinning事件发生时把完整调用栈直接打印到标准错误流里。这个方案适合本地复现问题和短时间压测,不建议在生产环境长期开启全量打印。

JFR jdk.VirtualThreadPinned 事件把 Java 21 虚拟线程、carrier 和锁内阻塞调用栈串起来的证据面板

修复动作:把等待逻辑移出锁范围,只替换真正有问题的锁

最稳妥的第一步优化,是把远端请求逻辑直接移出临界区,锁范围内只做极短时间的内存状态交换:

final class ProfileCache {
    private final ReentrantLock lock = new ReentrantLock();
    private volatile String value;

    String refresh(String userId) throws Exception {
        String fresh = loadRemoteProfile(userId); // 不占用共享锁等待网络
        lock.lock();
        try {
            value = fresh;
            return value;
        } finally {
            lock.unlock();
        }
    }
}

如果业务逻辑必须保证“同一个用户同一时间只能发起一次缓存刷新请求”,可以把锁的范围缩小到状态检查和状态变更这两步,把网络请求的结果暂时放到独立的刷新状态对象里暂存。直接把 synchronized 全改成 ReentrantLock 不是万能方案:它只是能让虚拟线程在等待锁的过程中主动卸载让出平台线程,没法让一个本身就很慢的阻塞native方法凭空变快。

不要混淆 JDK 21 和 JDK 24 的虚拟线程行为边界

Java 21 官方文档明确把 synchronized 代码块内部的长时间阻塞列为需要重点关注的pinning场景,并且给出建议:确认存在频繁、长时间的pinning问题时,可以考虑换成 ReentrantLock 实现。JEP 491 在JDK 24正式交付之后,虚拟线程阻塞在synchronized相关路径的可扩展性有明显改善,同一套压测用例在升级之后得到的结果可能完全不同。

这不代表我们可以直接删掉所有相关监控。JFR排查的习惯仍然值得保留;native方法调用、外部函数调用、过长的锁持有时间,还有下游依赖本身的延迟波动,仍然有可能制造出长尾请求。两次验证要使用完全相同的并发参数、完全相同的远端响应延迟和完全同一套JFR事件采集规则,测出来的结果才有可比性。

上线前的防复发检查清单

  • 把所有可能访问网络、磁盘、数据库或者native方法的调用,从 synchronized 类型的临界区里全部标记出来。
  • 用固定并发数和固定的下游依赖延迟做压测,对比改造前后的P95、P99延迟,锁等待总时长和carrier线程使用率。
  • 留存一份短时JFR录制文件,检查所有 jdk.VirtualThreadPinned 事件是不是集中出现在同一个方法里。
  • 升级JDK大版本之后必须重新做一遍验证,不要直接把Java 21下得到的结论直接套用到JDK 24及以上版本。

相关问题

所有 synchronized 都要替换成 ReentrantLock 吗?

不需要。短小、低频、只操作内存的临界区,完全没必要为了理论上存在的pinning概率去改写代码;优先处理调用频率高、内部包含阻塞操作的路径就可以。

VirtualThreadPinned 事件超过 20ms 就一定是故障吗?

不一定。它只是用来定位问题的线索,不能单独作为故障判定标准。要结合请求尾延迟、调用频率、锁竞争情况和外部依赖耗时几个维度综合判断,才知道它会不会真的影响服务吞吐。

JDK 24 之后还需要检查 pinning 吗?

需要。JEP 491只是优化了synchronized相关的pinning场景,native方法调用或者外部函数调用导致的其他pinning场景仍然存在,升级之后还是要做一次相同条件下的压测验证。

把一次慢请求还原成可验证的完整等待链

虚拟线程的价值,是用更轻量的方式承载大量等待型请求,不是用来绕开锁、网络和native调用本身的固有成本。排查问题的时候先理清楚三个点:是哪个虚拟线程被pin了、它占住了哪个承载线程、阻塞操作是不是真的处在高频率调用的临界区里。把JFR采集到的调用栈、锁的范围边界和下游依赖耗时三个信息串起来,修复方案才不会只停留在“把线程池大小调大”的表面操作上。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python typing.Protocol 运行时检查为什么不等于接口完整性:runtime_checkable、属性访问与静态类型边界Python typing.Protocol 运行时检查为什么不等于接口完整性:runtime_checkable、属性访问与静态类型边界
上一篇
Python typing.Protocol 运行时检查为什么不等于接口完整性:runtime_checkable、属性访问与静态类型边界
Go io.TeeReader 记录请求体为什么会拖慢接口:同步写入、失败传播与流式复制边界
下一篇
Go io.TeeReader 记录请求体为什么会拖慢接口:同步写入、失败传播与流式复制边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    4795次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4385次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4330次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4569次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4512次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码