传统 Java Web 应用的并发能力长期受限于平台线程的数量。Tomcat 默认线程池通常只有两百个工作线程,一旦某个请求涉及缓慢的下游调用,线程被长时间占用,整个应用的处理能力就会急剧下降。Project Loom 带来的虚拟线程正是为了解决这个问题:它把线程的调度权从操作系统收回到 JVM 手中,用极低的代价挂起和恢复线程,让“一个请求一个线程”这种简单直观的编程模型重新变得可行。本文将从原理、配置和实测三个层面,带你完整了解 Spring Boot 中如何使用虚拟线程。

一、虚拟线程的底层原理
要理解虚拟线程的价值,首先要明白它和平台线程的本质区别。平台线程是对操作系统线程的一比一封装,创建、销毁、切换都需要陷入内核,代价高昂。在典型的 Linux 系统上,单个线程默认预留约 1MB 的栈空间,加上内核调度的开销,一台普通服务器能承载的线程数量通常在几千到一万这个量级。当线程数超过 CPU 核心数太多时,上下文切换的成本会显著侵蚀系统吞吐量。
虚拟线程则完全不同。它由 JVM 自行管理,栈空间存放在堆内存中,初始占用通常只有几百字节,并且会按需增长和收缩。虚拟线程的调度不依赖操作系统,而是由 JVM 内置的 ForkJoinPool 完成,默认工作线程数等于 CPU 核心数。当一个虚拟线程执行到阻塞操作(比如网络 IO、Thread.sleep、LockSupport.park)时,JVM 会自动把它从载体线程上卸载,把载体线程让给其他就绪的虚拟线程,等阻塞结束后再重新挂载执行。
关键点在于:这一切对开发者是透明的。你仍然可以按照同步阻塞的方式写代码,不需要引入响应式框架的复杂编程模型,就能获得接近异步的资源利用率。这正是虚拟线程最大的卖点——用最小的代码改造成本换取高并发能力。不过要注意,少数操作会导致虚拟线程被“钉住”(pin)在载体线程上无法卸载,例如在 synchronized 块中执行阻塞 IO(JDK 24 之前的版本),或者调用本地方法。被钉住的虚拟线程会退化为平台线程的行为,高并发下可能耗尽载体线程。
二、在 Spring Boot 中启用虚拟线程
从 Spring Boot 3.2 开始,只要应用运行在 JDK 21 及以上版本,开启虚拟线程只需一行配置。Spring Boot 提供了 spring.threads.virtual.enabled 参数,设置为 true 后,Tomcat 会使用虚拟线程处理每个请求,@Async 异步任务、@Scheduled 定时任务的执行器也会自动切换为虚拟线程执行器。
完整的配置文件如下:
spring:
threads:
virtual:
enabled: true
server:
tomcat:
threads:
max: 200 # 该配置在虚拟线程模式下不再起限制作用
如果你希望更精细地控制,也可以手动创建虚拟线程执行器。JDK 提供了 Executors.newVirtualThreadPerTaskExecutor() 方法,为每个任务分配一个独立的虚拟线程。下面的示例展示了如何在自定义线程池场景中使用它:
@Configuration
public class VirtualThreadConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean(name = "batchExecutor")
public ExecutorService batchExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
有一个常见的误区需要提醒:开启虚拟线程并不意味着所有的并发问题都消失了。虚拟线程只是降低了线程创建的成本,线程安全、共享状态竞争这些问题依然存在。尤其是那些依赖 ThreadLocal 缓存重对象的代码,在百万级虚拟线程的场景下可能引发内存暴涨,需要提前排查。另外,如果应用中大量使用了 synchronized 包裹 IO 操作的旧代码,建议改用 ReentrantLock,避免钉住问题影响性能。
三、性能压测对比实验
理论说得再多,不如实测一次来得直接。我搭建了一个简单的 Spring Boot 应用,核心接口模拟一次耗时的下游调用(休眠 200 毫秒后返回),测试环境为 4 核 8G 的云服务器,JDK 21,分别用平台线程和虚拟线程两种模式跑压测。压测工具使用 wrk,并发连接数设为 2000,持续 60 秒。
测试用的接口代码非常简单:
@RestController
public class DemoController {
@GetMapping("/slow")
public String slow() throws Exception {
// 模拟一次耗时的远程调用
Thread.sleep(200);
return "ok";
}
}
两组测试的结果差异相当明显。平台线程模式下,Tomcat 默认 200 个线程全部打满,大量请求排队等待,最终吞吐量约为 950 请求/秒,平均延迟接近 1 秒,p99 延迟超过 2 秒。切换到虚拟线程后,吞吐量直接跃升到约 9600 请求/秒,平均延迟稳定在 210 毫秒左右,p99 延迟 260 毫秒,几乎就是接口本身的处理耗时。这个结果符合理论预期:接口是 IO 密集型,200 毫秒的休眠期间,虚拟线程全部被卸载,载体线程始终在处理新请求,2000 个并发连接毫无压力。
内存方面同样值得注意。平台线程模式下 200 个线程大约占用 200MB 以上的栈内存;而虚拟线程模式下,数万个活跃虚拟线程的总内存占用仅几十 MB,因为虚拟线程阻塞时栈会被移到堆中压缩存储。但是要强调一点:如果接口是 CPU 密集型(比如大量计算、加解密),虚拟线程几乎不会带来任何提升,因为瓶颈在 CPU 本身而不是线程数量。虚拟线程的收益场景明确指向阻塞 IO 占主导的应用,例如调用数据库、REST 接口、消息队列的典型 Web 服务。
四、使用建议与注意事项
综合上面的分析,给出几条实践建议。第一,升级到 JDK 21 和 Spring Boot 3.2 是前提条件,老版本项目需要先完成框架升级。第二,开启配置前先梳理代码中的 synchronized 使用情况,将包含阻塞 IO 的同步块替换为 ReentrantLock。第三,谨慎评估 ThreadLocal 的使用,尤其是用 ThreadLocal 缓存 SimpleDateFormat、数据库连接这类重对象的场景,可以改用共享的无状态对象或对象池。
连接池配置也需要重新审视。过去为了匹配有限的线程数,数据库连接池大小往往设置得和线程池相近;虚拟线程时代,请求数可能远超连接数,连接池反而成为新瓶颈。这时应结合下游数据库的实际承载能力设置合理上限,并利用连接池的超时和排队机制保护后端。同样,如果下游服务本身处理能力有限,虚拟线程带来的超高并发可能瞬间压垮下游,必要时需要在网关或客户端层面做限流。
最后总结一下:虚拟线程是 Java 并发编程的一次范式级升级,它让你用最朴素的同步代码写出高吞吐的服务,无需响应式框架的学习成本。对于以阻塞 IO 为主的 Spring Boot 应用,开启 spring.threads.virtual.enabled=true 几乎是零成本的性能红利。但它不是银弹,CPU 密集型任务、钉住问题、连接池瓶颈都需要逐一排查。建议先在测试环境充分压测,确认收益后再推往生产环境。
Spring Boot虚拟线程Project Loom修改时间:2026-09-08 22:07:21