【背景】
JDK 21 把虚拟线程推成 LTS 默认能力,Spring Boot 3.2 之后 spring.threads.virtual.enabled=true 一行就能把 Tomcat 线程池换成虚拟线程。我这边一个订单聚合服务(Spring Boot 3.3 + JDK 21 + HikariCP + OkHttp)正在做重构,团队里立刻分成两派:
- 派 A:新业务一律同步写法 + 虚拟线程,可读性和排错成本都低;响应式是"为异步而异步",背压在实际业务里 95% 用不上;
- 派 B:响应式在 I/O 密集 + 长连接 + 流式场景更省内存,Reactor 的调度器模型比虚拟线程更可控;虚拟线程遇到
synchronized或 JNI 调用会 pin 载体线程,高并发下反而更糟; - 派 C:两者并存,按链路特征选——同步 I/O 密集用虚拟线程,流式 / 背压 / 长连接用响应式,团队内统一一个就行。
我踩过的坑:把一段 synchronized 缓存 + 一次 Thread.sleep 的重型逻辑放到虚拟线程池里,压测时 P99 直接翻 3 倍,-Djdk.tracePinnedThreads=full 一开,pin 日志刷屏,最后把锁换成 ReentrantLock 才恢复。这件事让我对"虚拟线程=免费午餐"的说法打了个大问号。
【常见分歧 / 几种做法】
- 新服务默认虚拟线程 + 传统 MVC:写起来像同步,阻塞 IO 不再需要回调;代价是连接池、下游超时、重试这三件事必须做对,否则只是把"线程池耗尽"换成"虚拟线程数爆表 + 载体线程池耗尽"。
- 新服务默认响应式(WebFlux / R2DBC / WebClient):内存占用在 I/O 密集场景确实低,但团队要能读懂
flatMap/zip/concatMap的调度语义,排错链路变长;R2DBC 生态目前对复杂 SQL、存储过程、事务隔离的支持仍不如 JDBC 成熟。 - 老业务从响应式迁回虚拟线程:代码可读性大幅提升,但 Reactive 类型会渗透到 DTO / 仓储层,迁移成本主要在领域层,不是"换个 Starter"就能解决。
- 老业务从同步迁到响应式:一般是反向操作,很少见,除非有明确的背压 / 流式需求(比如网关聚合多个下游 + 背压到客户端)。
【我的倾向与理由】
- 新业务(无历史包袱、无强流式需求):默认虚拟线程 + 传统 MVC + JDBC + OkHttp。理由:可读性、可观测性(栈深友好、Arthas / JFR 直接可用)、生态成熟度这三项加起来,对 80% 的 CRUD 型后端收益远大于响应式。
- 需要背压 / 流式 / 长连接 / 高并发网关聚合:仍然用响应式,但要把它当"局部组件"而不是"全局范式",能少写
flatMap就少写。 - 老业务:不主动迁。响应式迁同步 ROI 通常为正(代码变短、排错变快),同步迁响应式 ROI 通常为负,除非有明确的内存 / 背压指标驱动。
- 无论哪种,三件事必须做:a) 连接池 / 信号量 / 下游限流做对,虚拟线程不解决下游打爆的问题;b) 把
synchronized在热路径上换成ReentrantLock或用ReentrantLock的tryLock兜底;c) 压测必须同时压虚拟线程数、载体线程数、下游 P99 三个指标,只盯吞吐会漏掉 pin 问题。
【想听大家在什么场景下怎么选】
- 你们现在新服务的默认选择是什么?虚拟线程、响应式、还是"看人下菜"?
- 有没有从响应式迁到虚拟线程(或反向)的真实数据?最好带上内存占用、P99、吞吐、GC 停顿的对比,别只说"变快了"。
- 遇到 pin、虚拟线程数爆表、下游超时叠加这三种组合故障,你们是怎么观测和治理的?JFR 的哪个事件最能定位 pin?
- R2DBC / JDBC 在你们生产里的踩坑对比,尤其是事务和连接池这两块。
欢迎带上具体版本(JDK / Spring Boot / Reactor / HikariCP)和压测参数,空对空的"响应式 vs 虚拟线程"争论我这边已经看到很多,想听点带数据的。
0赞
0收藏
0楼层