当前位置:
AIGC文章详情

Spring Boot 4 默认开启虚拟线程:10 倍 QPS 是真的,第 8 天线上翻车也是真的

源自156位全网作者

13:22

最近刷技术社区,虚拟线程的话题又热起来了,但评论区的风向很分裂。

一边是晒战绩的:有团队把服务切到虚拟线程,QPS 从 800 干到 8000+,CPU 和内存都没什么异常,作者当场发朋友圈"虚拟线程真香"。知乎

另一边是晒事故的:同样是虚拟线程上线,第一周跑得挺好,第八天下午流量高峰,P99 响应时间从 200ms 飙到 8 秒,QPS 反而从 8000 掉到 2000,CPU 却只有 30%——监控看起来就像服务在"故意"拒绝处理请求。知乎

两边说的都是真事,而且都发生在最近几个月。这就是今天想聊这个话题的原因:虚拟线程不是"要不要用"的实验室话题了,它正在变成一个躲不开的默认选项。

为什么偏偏是现在

先把时间线摆出来,你就明白为什么这个窗口期值得认真对待:

  • Spring Boot 4.x 在 Java 21+ 环境下,会自动用虚拟线程替换 Tomcat 的工作线程池,等效于默认开启 spring.threads.virtual.enabled=true,你一行代码都不用改;

  • Spring Boot 4.1.0-RC1 已在 2026 年 4 月 23 日发布,带 113 项改进。知乎GA 不会太远;

  • 3.5.x 是 3.x 的最后一个大版本,进入维护窗口期只是时间问题,“升不升"已经变成"什么时候升”;

  • JDK 25 作为 LTS 版本,顺带修掉了虚拟线程最要命的一个坑(后面细说)。

也就是说:不升,等 3.x 停止维护后会被动;闭眼升,就是把别人踩过的坑在生产环境重演一遍。两头都不划算,中间的路是搞清楚再升。

先说清楚:虚拟线程到底改变了什么

传统 Java 平台线程和操作系统线程是 1:1 绑定的,每个线程默认吃 512KB 到 1MB 栈内存,Tomcat 的 maxThreads 默认 200——这不是拍脑袋的数字,而是多年调优的平衡点:线程再多,上下文切换的开销就吃掉收益了。知乎所以"单机几百并发"成了大多数 Java 服务的隐形天花板。

虚拟线程把这层天花板掀了:虚拟线程执行到阻塞操作(数据库查询、下游 HTTP 调用、Thread.sleep)时,JVM 会自动把它从载体线程上卸载,让载体线程去跑别的虚拟线程,等 IO 完成再挂载回来。结果就是 Web 层能接住的并发请求从"几百"跳到"几万"。

Spring Boot 4 默认开启虚拟线程:10 倍 QPS 是真的,第 8 天线上翻车也是真的

但注意这句,也是所有翻车事故的共同根源:虚拟线程解除的是"Web 层能接多少请求"的上限,不是"下游能处理多少请求"的上限小红书

坑一:一个 synchronized,把吞吐量钉回原地

先看那个第八天翻车的完整故事,细节很有代表性。

告警出现后,排查的人先怀疑数据库慢——慢查询日志里确实有几条超过 1 秒的查询,但数量不至于把 50 个连接的池子全占住。用 jstack 抓线程快照,发现了真正诡异的地方:8300 个虚拟线程处于 BLOCKED 状态知乎这不对——虚拟线程遇到 IO 阻塞应该是"让出"载体线程,状态是 WAITING,不该是 BLOCKED。

再看这 8300 个线程在等什么:BLOCKED on a monitor lock。它们全在等一个 synchronized 锁。翻到代码,是一个订单服务里的 synchronized 块,里面包着网络调用和数据库操作。

这就是虚拟线程著名的 pinning 问题:虚拟线程在 synchronized 代码块内阻塞时,无法让出载体线程,载体线程被"钉住"干等。知乎200 个载体线程全被钉住,后面几万个虚拟线程只能排队;更糟的是,被钉住的虚拟线程在进锁之前已经拿到了数据库连接,一直占着不释放,连接池随之耗尽——这就是"CPU 只有 30%、QPS 却暴跌"的解释:大家什么都没干,全在排队。

Spring Boot 4 默认开启虚拟线程:10 倍 QPS 是真的,第 8 天线上翻车也是真的

修复方案分两层:

  • 治标:把 synchronized 换成 ReentrantLock。它走 JUC 的 park/unpark 机制,虚拟线程阻塞时可以正常让出载体线程。这个团队改完重新部署,P99 从 8 秒降回 200ms,QPS 恢复 8000+。顺带一提,他们复盘时发现那个锁保护的"查库存 + 写订单"根本没有需要原子保护的共享状态,属于前人随手加的粗粒度锁,最后一并把锁删了。

  • 治本:升级 JDK。synchronized 导致 pinning 的问题在 JDK 24 通过 JEP 491 修复,JDK 25 LTS 自然包含。知乎如果你的 Spring Boot 4 还跑在 JDK 21 基线上,这个坑是原封不动存在的。

还有个坏消息:pinning 在开发环境基本测不出来,因为并发量不够;只有生产环境的高并发才会让它现形。所以上线前最好用 -Djdk.tracePinnedThreads 参数在预发环境把 pinning 日志打出来,生产上用 JFR 持续记录,再配一个 pinning 频率的监控告警。

坑二:连接池成了新瓶颈,而且默认配置必踩

第二个坑更普遍,因为它不需要你的代码有任何"历史包袱"。

HikariCP 的设计哲学是"连接数越少越好",默认 maximumPoolSize 只有 10,官方建议单服务别超过 20-30。知乎这个设计在传统架构下非常合理:你总共只有 200 个 Tomcat 线程,它们也不可能同时都在做数据库操作,10-20 个连接绰绰有余。

虚拟线程改变了这个前提。现在 Tomcat 能同时接住几万个请求,假设 5000 个请求同时到达、全都要查库——连接池只有 10 个。剩下 4990 个请求全部挂起等连接,等满默认 30 秒超时,一波 SQLTimeoutException 涌出来。知乎虚拟线程把 IO 并发能力放大了上百倍,但连接池容量没跟上,瓶颈直接换了个位置爆炸。

修复是两个方向一起做:

  • 调大连接池:从 HikariCP 官方公式(核心数 × 2 + 有效磁盘数)起步,虚拟线程场景一般拉到 30-50,但别头脑一热拉到 200+——数据库端的每个连接都是真金白银的内存开销,连接过多反而拖慢整体;

  • 开启惰性连接:Spring Boot 4.1 内置了 LazyConnectionDataSourceProxy 的自动集成,通过 spring.datasource.connection-fetch 配置启用——只有真正执行 SQL 时才占用物理连接。那些"开了事务但先读缓存、命中就返回"的代码路径,能省掉大量无效连接占用。

两项叠加,有实践者给出的结论是:可以用比传统架构少四分之三的连接数,服务更多的并发请求。知乎

坑三:瓶颈没有消失,只是搬家了

最后这个坑是认知层面的,也是最容易让人做出错误决策的。

有较真的开发者算过一笔账:你的连接池只有 50 个连接,下游服务只允许 80 个并发调用,现在换成虚拟线程压进来 1000 个请求——这 1000 个请求会在哪里等待、争用或被拒绝?知乎答案不是"虚拟线程很轻所以都能扛住",而是:它们会在连接池门口排队,在下游限流器门口排队。

虚拟线程改变的是排队的位置,不是总工作量。知乎想明白这一点,就能理解社区里两个看起来矛盾的现象:

一是"为什么感觉 Java 圈对虚拟线程没什么热度"——这个问题在知乎上被直接问了出来。高赞区里,连力推虚拟线程的实战派作者都承认,自己的团队也只是计划把 JDK 25 升级排进下半年维护窗口。知乎对更多版本还没到 21+ 的团队来说,虚拟线程更是暂时与他们无关——真正的门槛从来不是"想不想用",而是"升不升得动"。

二是"Java 都有虚拟线程了,为什么还有人用 Go"——因为并发模型的瓶颈早就不在语言层的线程开销上了,而在你的数据库、下游接口和连接池容量上。换语言不免除这笔账。知乎

Spring Boot 4 默认开启虚拟线程:10 倍 QPS 是真的,第 8 天线上翻车也是真的

到底该不该升?对号入座

值得升的:服务是 IO 密集型(大量时间花在等数据库、等下游 HTTP、等 Redis),且能直接上 JDK 25。这是虚拟线程收益最实在的人群,不升等于把免费的吞吐量扔在地上。

先缓一缓的:还在 JDK 21 基线、代码里全是年代久远的 synchronized、依赖的第三方库里锁多且不可控。pinning 对这种代码库是暗雷,建议先把 JDK 升到 25,再开虚拟线程,顺序别反。

不用凑热闹的:CPU 密集型服务——图像处理、加解密、复杂规则引擎。虚拟线程省的是"等待"的开销,你的瓶颈在计算,切了基本白切。知乎

一个兜底选项:如果团队还没评估完,Spring Boot 4.x 里可以用 spring.threads.virtual.enabled=false 显式关掉虚拟线程,先把版本升上去,把虚拟线程的影响评估和连接池调优放到下个迭代。这是风险最小的升级姿势。

升级前过一遍这张清单

  1. JDK 版本优先到 25 LTS,别用 21 硬扛虚拟线程上线;

  2. 全局搜 synchronized,确认锁内没有网络调用和数据库操作;拿不准的,预发环境开 tracePinnedThreads 跑一轮高并发;

  3. 检查 ThreadLocal 的使用——虚拟线程数量暴涨后,ThreadLocal 的内存和行为都要重新评估;

  4. 连接池参数重算:30-50 起步,配上 4.1 的惰性连接,别超过数据库能承受的并发上限;

  5. 压测必须用接近生产的并发量,开发环境那点并发永远测不出 pinning;

  6. 上线后盯三个信号:pinning 事件频率、连接池等待时间、P99 延迟。任何一个异常,先怀疑虚拟线程与锁/连接池的交互,再去怀疑数据库。

后面还值得盯什么

接下来几个月有两件事值得留意:一是 Spring Boot 4.1 GA 的落地时间,GA 之后"默认虚拟线程"会真正铺开到大量团队,踩坑报告和最佳实践会密集出现;二是结构化并发(Structured Concurrency)的进展——虚拟线程解决了"线程贵"的问题,但"怎么管理成千上万个并发任务的生命周期"正在成为下一个讨论焦点,社区里的并发系列教程已经开始把它当收官内容来讲了。知乎

Spring Boot 4 默认开启虚拟线程:10 倍 QPS 是真的,第 8 天线上翻车也是真的

一句话总结:虚拟线程这波红利是真的,但它发的是"懂容量账的人"。框架替你开了开关,连接池、锁和下游容量的账,还是得自己算。

内容由AI生成
0
扫一下,分享更方便,购买更轻松
0评论

当前文章无评论,是时候发表评论了
提示信息

取消
确认
评论举报

最新文章 热门文章