【背景】 做了 15 年后端,最纠结的一个架构决策就是「线程模型」。Java 21 把 Virtual Threads 正式带入生产,但很多团队还在用固定线程池 + 阻塞 I/O。我见过两种真实案例:
案例 A:某支付网关,Java 17 + HikariCP + 固定 200 线程池,稳态 QPS 5000,P99 稳定 80ms。但下游 MySQL 一次 200ms 的慢查询,线程池瞬间打满,P99 飙到 2.3s,持续 40 秒才恢复。事后复盘:200 个线程全卡在 DB 连接等待,新请求排队,队列堆积到 8000。
案例 B:某内容平台,Java 21 + Virtual Threads,QPS 30000,P99 50ms。但上线后第 3 天出了一次 OOM,排查了整整 3 天:根因是一个第三方 SDK(某对象存储客户端)内部用了 synchronized 方法锁,在 Virtual Threads 下,一旦 carrier thread 被 synchronized 阻塞,所有等待该锁的 Virtual Thread 都卡住(pinned),最终 carrier thread 耗尽,触发 OOM。
【常见分歧——四种做法】
- 全量 Virtual Threads:简单粗暴,Java 21 默认
spring.threads.virtual.enabled=true,全链路统一。优点是吞吐上限高(I/O 等待不占 carrier thread);风险是任何一处 synchronized 都是定时炸弹。 - 分路径处理:I/O 密集路径(RPC、DB、Redis)走 Virtual Threads,CPU 密集路径(加密、压缩、JSON 序列化)走固定平台线程池。这是我最推荐的,但实现上需要显式隔离。
- 保守派:继续用线程池 + 非阻塞 I/O(Netty/Reactor):性能上限不低,但开发心智负担重,调试链路复杂,团队门槛高。
- 保守派 +2:线程池 + 阻塞 I/O + 大池子:最朴素,但正如案例 A 所示,下游一抖就雪崩。
【我的倾向与理由】 核心判断标准是 I/O 等待占比:
- I/O 等待占比 > 70%(典型:DB 查询 + RPC 调用为主的业务接口)→ Virtual Threads 无脑选,收益是量级的(线程数可以开到 10 万级别而不 OOM)
- I/O 等待占比 < 30%(典型:大量 CPU 计算、加密、压缩)→ 平台线程池,Virtual Threads 没有优势,反而有 pinning 风险
- 混合负载(60%~70% 边界)→ 分路径处理,I/O 走 Virtual Threads,CPU 走固定池
三个必须验证的前提(踩坑清单):
- 全链路 grep
synchronized,替换为ReentrantLock(JDK 19+ 已优化大部分 JDK 内部锁,但第三方库不受控) - 不能用
ThreadLocal(Virtual Thread 下每个线程一个 ThreadLocal 对象,10 万线程 = 10 万份内存),换成ScopedValue(JDK 21 预览 / 22 正式) - 第三方库(DB 驱动、对象存储 SDK、连接池)必须做过 Virtual Threads 兼容性压测——这是案例 B 的根因
【想听大家在什么场景下怎么选】
- 你们线上有跑 Virtual Threads(Java 21+)的吗?什么版本、什么场景、QPS 和 P99 数据?
- 有没有踩过 synchronized 导致 carrier thread pinned 的坑?怎么排查的?用了什么工具(jcmd Thread.dump / async-profiler)?
- 监控方面,Virtual Threads 的健康度怎么观测?你们用什么指标判断「carrier thread 快耗尽了」?有没有好用的 Grafana dashboard 可以分享?
- 如果你们还在用 Java 17 / 11,升级 21 的最大阻力是什么?(兼容性?团队认知?第三方库?合规要求?)
0赞
0收藏
0楼层