前言
Java 21 里最吸引人的特性之一就是虚拟线程。以前写高并发接口,大家很容易想到线程池、异步回调、CompletableFuture、响应式编程。虚拟线程出来以后,很多阻塞式代码也有了新的选择。
但我不太赞成一升级 Java 21,就把所有线程池都换成虚拟线程。它很好,但不是所有场景都适合。
这篇主要记录一下我对虚拟线程使用边界的理解,尤其是放在 Spring Boot 项目里时,哪些地方可以尝试,哪些地方要谨慎。
它解决的是什么问题
传统平台线程比较重。一个请求如果在等待数据库、等待 HTTP 接口、等待 Redis,线程也会被占着。并发一高,线程数量、上下文切换和内存都会成为压力。
虚拟线程的目标是让“一个任务一个线程”变得更便宜。代码仍然可以写成同步阻塞的样子,但等待 IO 时不会一直占着昂贵的平台线程。
比如这种代码:
public OrderDetail getDetail(String orderId) {
Order order = orderClient.getOrder(orderId);
PayInfo payInfo = payClient.getPayInfo(orderId);
return new OrderDetail(order, payInfo);
}
从代码阅读上看,它比一堆回调更直接。虚拟线程的价值就是让这种同步写法在 IO 密集场景下有更好的伸缩性。
Spring Boot 里怎么打开
Spring Boot 3.2 开始对虚拟线程支持更直接。配置上可以开启:
spring:
threads:
virtual:
enabled: true
开启以后,Web 请求处理等场景会使用虚拟线程。不过这不代表项目里所有问题都解决了。
虚拟线程只是执行模型变化,数据库连接池、HTTP 连接池、第三方接口限流都还是原来的资源。线程变便宜了,不代表下游资源无限。
IO 密集场景更适合
我觉得虚拟线程最适合的是 IO 密集场景。
比如:
1.调用多个内部 HTTP 服务。 2.查询数据库后组装结果。 3.读取对象存储或文件服务。 4.等待第三方接口返回。
这些场景里,线程大部分时间在等。虚拟线程能让等待成本降低。
但如果是 CPU 密集计算,比如大量加密、压缩、复杂报表计算,虚拟线程不会让 CPU 变多。这时更应该控制并发,避免把机器打满。
数据库连接池不能忘
很多人试虚拟线程时会遇到一个问题:接口线程好像不缺了,但数据库连接池先爆了。
这很正常。以前 200 个平台线程可能天然限制了并发请求数量,现在虚拟线程能轻松创建很多,但数据库连接池还是只有几十个连接。
比如 Hikari 连接池:
spring:
datasource:
hikari:
maximum-pool-size: 30
如果请求量很大,大量虚拟线程会一起等连接。它们本身不贵,但数据库并不会因为线程便宜就能承受更多查询。
所以引入虚拟线程后,更要看连接池、慢 SQL、下游限流。不要只看应用线程数。
ThreadLocal 要谨慎
老项目里经常用ThreadLocal保存用户信息、租户信息、traceId。
虚拟线程也支持ThreadLocal,但因为虚拟线程数量可能很多,如果滥用 ThreadLocal,内存和上下文管理会变得更复杂。
我的习惯是能显式传参就显式传参,必须放上下文的再放。比如日志 traceId 可以继续交给成熟的链路追踪方案,不要自己到处手写。
同时要注意线程池混用。如果某段代码从虚拟线程切到自定义平台线程池,上下文不一定自动传过去。
不要盲目替换业务线程池
项目里常见一些自定义线程池,比如导入任务线程池、消息消费线程池、报表生成线程池。
这些线程池通常不只是为了创建线程,也是为了限流。比如批量导入最多同时跑 5 个任务,不是因为线程不够,而是因为数据库和业务资源只能承受这么多。
如果直接改成虚拟线程,可能会把限流边界打破。
所以我会区分两种情况:
1.只是为了处理大量 IO 等待,可以考虑虚拟线程。 2.本身承担限流职责,就要保留并发控制。
线程便宜不等于业务可以无限并发。
小结
虚拟线程是 Java 21 很重要的能力,尤其适合 Spring Boot 里大量同步 IO 的业务代码。它让代码可以保持直观写法,同时改善线程资源成本。
但它不是性能万能药。数据库连接池、HTTP 连接池、下游限流、CPU 使用率都要一起看。我的建议是先在 IO 密集、边界清楚的接口上试,再逐步扩大范围,不要一上来把所有执行模型都改掉。
喜欢的话,留下你的评论吧~