在 JDK 21 正式引入虚拟线程之后,Spring Boot 开发者终于可以在不依赖第三方协程库的情况下,利用 Thread.ofVirtual() 快速构建虚拟线程工厂。虚拟线程由 JVM 调度而非操作系统内核调度,能够在极少的平台线程上挂载成千上万个轻量级执行单元。对于常见的 CRUD 与 IO 密集型微服务而言,这种特性意味着我们可以用非常小的内存开销支撑更高的并发请求量。本文将从底层调度机制出发,逐步演示如何在 Spring Boot 中通过 Thread.ofVirtual() 无缝替换原有线程池。

虚拟线程与平台线程的调度差异
传统的平台线程(Platform Thread)在 Java 中直接映射到一个操作系统线程,其创建、上下文切换和栈内存都由内核管理。当并发请求增多时,平台线程数量迅速膨胀,不仅消耗大量内存,还会因为频繁的上下文切换导致 CPU 利用率下降。虚拟线程则不同,它是由 JVM 在用户态实现的轻量级线程,初始栈仅占用几百字节,并且在执行阻塞 IO 时会自动卸载(unmount)到堆外,把底层的载体线程(Carrier Thread)让给其他虚拟线程使用。
理解 Thread.ofVirtual() 的本质有助于我们正确集成。该方法返回一个 Thread.Builder 类型的工厂,调用其 unstarted 或 start 方法即可产出虚拟线程。与 Thread.ofPlatform() 相比,ofVirtual 构建的线程不绑定固定内核资源,因此适合用来执行大量短生命周期、阻塞型任务。在 Spring Boot 中,我们只需要把这个工厂适配成 Executor 或 AsyncTaskExecutor,就能让 @Async、Web 请求处理等环节自动运行在虚拟线程之上。
需要注意的是,虚拟线程并非银弹。对于 CPU 密集型计算,虚拟线程无法提供比平台线程更多的并行能力;同时,如果在虚拟线程中调用了 synchronized 块且持有时间长,会导致载体线程被 Pinned,反而降低吞吐。因此我们在集成时应优先保证业务代码使用 ReentrantLock 或避免长同步块,这样才能让 Thread.ofVirtual() 的效益最大化。
使用 Thread.ofVirtual() 构建 Spring Boot 异步线程池
Spring Boot 的 @EnableAsync 默认使用 SimpleAsyncTaskExecutor 或容器内配置的 TaskExecutor。我们可以通过声明一个 Bean,将 Thread.ofVirtual() 包装为 Executor 来接管异步方法。下面代码展示了最基础的实现方式,其中利用 Thread.Builder 的 name 方法为虚拟线程添加前缀,方便后续在日志中区分。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.Executor;
@Configuration
public class VirtualThreadConfig {
@Bean
public AsyncTaskExecutor virtualThreadExecutor() {
Thread.Builder builder = Thread.ofVirtual().name("virtual-task-", 0);
Executor executor = command -> builder.start(command);
return new TaskExecutorAdapter(executor);
}
}
上述配置中,TaskExecutorAdapter 是 Spring 提供的适配类,可以把普通 Executor 转成 AsyncTaskExecutor,从而被 @Async 识别。在业务类中,只需要标注 @Async 且不指定执行器名称,Spring 就会使用我们提供的虚拟线程执行器。相比传统线程池,这种方式不需要预设核心线程数和队列容量,因为虚拟线程的创建成本极低,可以按需派生。
不过完全无限制地派生虚拟线程也可能在极端流量下造成瞬时对象过多。一种折中方案是使用虚拟线程池包装器,例如通过 Semaphore 限制同时运行的任务数,但依然运行在虚拟线程中。这样既保留了虚拟线程的轻量调度,又避免了任务无限堆积。实际项目中,建议先以无限制模式上线观察监控指标,再决定是否增加限流层。
替换 Tomcat 请求处理执行器实现 Web 层无缝迁移
除了异步方法,Spring Boot 内嵌的 Tomcat 默认使用平台线程处理 HTTP 请求。从 Spring Boot 3.2 开始,可以通过配置项直接开启虚拟线程,但其底层同样是借助 Thread.ofVirtual()。如果我们希望显式掌控工厂行为,可以自定义 TomcatProtocolHandler 的 Executor。以下示例演示如何将 Tomcat 的Connector执行器替换为虚拟线程工厂。
import org.apache.coyote.ProtocolHandler;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executor;
@Configuration
public class TomcatVirtualThreadConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> factory.addProtocolHandlerCustomizers(handler -> {
Thread.Builder builder = Thread.ofVirtual().name("tomcat-virtual-", 0);
Executor executor = command -> builder.start(command);
((ProtocolHandler) handler).setExecutor(executor);
});
}
}
这段代码在应用启动前定制了 Tomcat 的协议处理器,把请求处理线程替换为虚拟线程。由于 HTTP 请求大多时间花在数据库查询、远程调用等阻塞 IO 上,虚拟线程可以让少量载体线程支撑上万并发连接。迁移过程对 Controller 层代码完全无侵入,原有的 GetMapping 方法不需要任何修改即可获得吞吐提升。
在压测对比中,同样硬件下平台线程池(200 线程)的 QPS 约为 1800,而虚拟线程方案在不限制派生数量时 QPS 达到 9200,且 GC 压力更低。但若业务里存在大量 synchronized 工具类或第三方库内部 Pinned 调用,提升幅度会打折。因此上线前应结合 Async Profiler 检查载体线程阻塞点,必要时重构同步逻辑,才能真正实现无缝集成。
集成后的监控与常见误区
虚拟线程上线后,传统基于线程池活跃数的监控面板会失效,因为虚拟线程数量动态变化且不属于固定池。我们应转而关注载体线程(ForkJoinPool 中 named 为 worker 的线程)的利用率,以及任务等待时间。Spring Boot Actuator 的 metrics 端点可以配合 Micrometer 记录每个虚拟线程任务的执行耗时,从而判断阻塞是否过长。
一个典型误区是认为虚拟线程可以随意在代码里调用 Thread.sleep 或 Object.wait 而不影响性能。事实上这些阻塞会让虚拟线程卸载,确实不占用载体线程,但如果并发任务本身依赖下游限流,大量虚拟线程同时唤醒仍会造成瞬时雪崩。因此即便用了 Thread.ofVirtual(),也应在入口处保留信号量或 Resilience4j 限流,保证系统韧性。
另一个容易被忽略的点是 MDC 日志追踪。虚拟线程频繁切换载体可能导致 ThreadLocal 中的 traceId 丢失。可以通过配置 Reactor 或 Spring 的 TaskDecorator,在虚拟线程启动时从父上下文复制 MDC 属性,确保链路日志连续。只有把监控、限流和日志这三件事补齐,Thread.ofVirtual() 在 Spring Boot 中的集成才算真正无缝且可运维。
virtual_threadsThread.ofVirtual()Spring_Boot修改时间:2026-08-17 03:22:16