news 2026/11/5 5:26:11

为什么 Spring Boot 3 的虚拟线程没让接口变快:JDK 21 pinning 排查记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
为什么 Spring Boot 3 的虚拟线程没让接口变快:JDK 21 pinning 排查记录

本文摘要:启用虚拟线程后接口吞吐不升反降,根因是请求路径上的native方法调用导致pinning。用tracePinnedThreads定位后将文件I/O迁移到NIO API即可解决。

一、问题与结论

Spring Boot 3.2 + JDK 21,Tomcat 10.1.15,8 核容器,200 并发压测。在application.yml中开启spring.threads.virtual.enabled=true后,QPS 不升反降,P99 延迟上升,实际并发度退化为 8(CPU 核心数),远低于平台线程模式下 Tomcat 默认的 200 并发。

结论:请求处理路径上存在new FileInputStream(path)调用,触发 JDK 21 的 native 方法 pinning——虚拟线程被钉在载体线程上无法卸载。8 条载体线程全部被占满后,调度器中排队的其余虚拟线程无法执行。虚拟线程的优势前提是"阻塞时能被卸载",pinning 使这个前提失效,虚拟线程退化为"每条载体线程只能同时处理一个任务",反而不如平台线程。

二、排查与根因

定位方法

启动参数加入 pinning 追踪:

java-Djdk.tracePinnedThreads=full-jarapp.jar

JDK 21 内置该检测机制,full参数打印完整线程栈,可定位到具体调用行。压测期间控制台持续输出 pinning 报告,关键栈帧:

java.io.FileInputStream.readBytes(Native Method)

Native Method标记说明 pinning 来自 native 方法调用,而非synchronized锁——这与大量已发表的 synchronized pinning 文章不同,本次触发点是一个看起来无害的文件读取。

pinning 机制

虚拟线程通过 ForkJoinPool 调度到载体线程执行,并行度默认与 CPU 核心数相关。当虚拟线程执行Thread.sleep()、LockSupport.parkNanos()等阻塞操作时,JVM 将其从载体线程卸载(unmount),载体线程转去执行其他虚拟线程。这是少量线程承载大量并发的核心机制。

native 方法调用打断了这一机制:native 代码直接在载体线程上执行,JVM 无法在调用中途调度或中断,载体线程被独占直到调用返回。FileInputStream.readBytes()、FileOutputStream.writeBytes()等均为 native 方法。

JEP 491(JDK 22)只修复了synchronized场景的 pinning。native 方法的 pinning 是虚拟线程模型的固有限制,升级 JDK 无法解决。

I/O API 对比

API底层实现触发 pinning
new FileInputStream(path)nativereadBytes()是
Files.newInputStream(path)NIOFileChannel否
new FileReader(path)包装FileInputStream是
Files.newBufferedReader(path)NIOFileChannel否

三、可运行示例

最小复现环境:JDK 21 + Spring Boot 3.2.0。配置application.yml开启虚拟线程:

spring:threads:virtual:enabled:true

Controller 包含两个端点,分别触发和规避 pinning:

packagecom.example.demo;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.nio.file.Files;importjava.nio.file.Path;importjava.util.Map;@RestControllerpublicclassFileIOController{privatefinalPathtestFile;publicFileIOController()throwsIOException{this.testFile=Files.createTempFile("vt-test",".txt");Files.writeString(testFile,"x".repeat(50000));}@GetMapping("/pinned-read")publicMap<String,Object>pinnedRead()throwsIOException{try(FileInputStreamfis=newFileInputStream(testFile.toFile())){byte[]buffer=newbyte[1024];while(fis.read(buffer)!=-1){}}returnMap.of("api","FileInputStream","pinning","expected");}@GetMapping("/unpinned-read")publicMap<String,Object>unpinnedRead()throwsIOException{try(InputStreamis=Files.newInputStream(testFile)){byte[]buffer=newbyte[1024];while(is.read(buffer)!=-1){}}returnMap.of("api","Files.newInputStream","pinning","none");}}

编译运行并发起并发请求:

mvn clean package-DskipTestsjava-Djdk.tracePinnedThreads=full-jartarget/demo.jar
foriin$(seq1200);docurl-shttp://localhost:8080/pinned-read>/dev/null&done;wait

四、验证结果与边界

预期输出:/pinned-read被请求后,控制台打印 pinning 完整线程栈,栈中可见java.io.FileInputStream.readBytes(Native Method)帧;/unpinned-read请求后不出现任何 pinning 报告。

实际输出:200 并发请求/pinned-read时每次触发 pinning 报告,/unpinned-read控制台无 pinning 输出。(本地环境实测,未做正式基准测试。)

常见失败:替换为Files.newInputStream()后问题依旧。原因通常是第三方库(模板引擎、序列化库)内部仍调用FileInputStream。解决方法:查看 pinning 栈的完整调用链确认来源;若该库无法替换,将该段文件 I/O 隔离到独立的平台线程池中执行。

升级 JDK 22 不解决:JEP 491 只修复synchronizedpinning,native 方法 pinning 依然存在。计划通过升级 JDK 一劳永逸解决 pinning 的团队,对 native I/O 场景需要另找方案。

何时不启用虚拟线程:如果瓶颈在数据库查询耗时或 CPU 密集计算,而非并发 I/O 等待,虚拟线程不会带来收益,反而增加排查 pinning 的成本。

五、替代方案与取舍

方案适用场景代价与边界
迁移到 NIO API文件 I/O 可控,能逐一替换调用点第三方库内部可能仍用FileInputStream,需逐一排查
文件 I/O 隔离到平台线程池无法替换的第三方库调用了 native I/O需额外管理线程池大小和生命周期
不启用虚拟线程瓶颈在 CPU 计算或数据库查询,不在 I/O 等待放弃虚拟线程的其他收益

不应使用本文方案的情况:文件 I/O 不在请求热路径上(如仅启动时读取配置),pinning 频率极低,迁移成本大于收益。

参考资料

  • JEP 444 — Virtual Threads
  • JEP 491 — Synchronize Virtual Threads without Pinning
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 5:21:51

基于springboot的校园自律碎片时间搭子匹配系统的设计与实现论文代码

学弟学妹们&#xff0c;大家好&#x1f44b;&#xff01;作为已经顺利上岸的计算机往届学长&#xff0c;当年独立调试 SpringBoot 后端接口、适配微信小程序交互逻辑、规整 MySQL 表结构、梳理整套系统业务与论文内容的过程依旧记忆犹新。非常清楚做校园互助类毕设的难处&#…

作者头像 李华
网站建设 2026/11/5 5:09:12

桌面窗口总乱糟糟?Seelen UI 平铺窗口管理与 Windows 桌面定制指南

桌面窗口总乱糟糟?Seelen UI 平铺窗口管理与 Windows 桌面定制指南 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI 窗口越开越乱、图标越攒越多,切一次虚拟桌…

作者头像 李华
网站建设 2026/11/5 5:07:56

免费上手 3D 高斯斑点编辑:SuperSplat 快速使用指南

免费上手 3D 高斯斑点编辑&#xff1a;SuperSplat 快速使用指南 【免费下载链接】super-splat 3D Gaussian Splat Editor 项目地址: https://gitcode.com/gh_mirrors/su/super-splat SuperSplat 是一款免费、开源、完全运行在浏览器里的 3D 高斯斑点编辑工具&#xff0c…

作者头像 李华
网站建设 2026/11/5 5:06:07

ADS122U04IPWR+PIC18F57K42工业级温度测量信号链设计

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

作者头像 李华
网站建设 2026/11/5 5:04:19

基于ADS122U04与STM32的工业级高精度温度测量方案设计

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

作者头像 李华
网站建设 2026/11/5 5:03:49

STM32F373外挂ADS122U04:HVAC高精度温度采集方案详解

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

作者头像 李华