Spring Boot 3.2+中启用虚拟线程需配置spring.threads.virtual.enabled=true,该配置自动替换Servlet容器(Tomcat/Jetty)线程池为虚拟线程执行器,覆盖请求处理全链路;手动创建虚拟线程仅局部有效,且须避免CPU密集型操作、升级下游组件(如WebClient、R2DBC)、显式处理MDC/ThreadLocal上下文传递。

直接替换 Executor 就能启用虚拟线程,但必须关掉 Tomcat/Jetty 的线程池
Java 21+ 的 Servlet 容器(如 Tomcat 10.1.21+、Jetty 12)默认仍用平台线程池处理请求。不改 Executor,Thread.ofVirtual() 或 Executors.newVirtualThreadPerTaskExecutor() 在业务层手动创建的虚拟线程只是“局部优化”,无法改变整个请求生命周期的调度模型。
关键动作是让容器把每个 HTTP 请求交给一个虚拟线程执行——这要求容器底层的 Executor 被替换成虚拟线程版。Spring Boot 3.2+ 提供了开箱配置:
-
spring.threads.virtual.enabled=true(application.properties)会自动将 Tomcat/Jetty 的连接器线程池替换为虚拟线程执行器 - 若用原生嵌入式服务器(如 JDK
HttpServer),需显式调用server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()) - Spring WebMvc(非 WebFlux)项目中,该配置仅影响 Servlet 容器层;Controller 内部调用仍走同步阻塞逻辑,无需改写
别在虚拟线程里做 CPU 密集型操作,否则会卡住整个载体线程池
虚拟线程只在 I/O 阻塞时被 JVM 自动卸载(unmount),释放载体线程。但如果它执行的是纯计算任务(比如大数组排序、JSON 解析、加密哈希),JVM 无法感知“阻塞”,会一直占用载体线程,导致其他就绪的虚拟线程饿死。
典型错误现象:Thread.sleep(100) 没问题,但 IntStream.range(0, 10_000_000).sum() 会让吞吐量断崖下跌。
- 识别 CPU 密集型操作:循环体无 I/O、无锁等待、无
Thread.yield()/LockSupport.park() - 正确做法:将这类任务提交到专用平台线程池,例如
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) - Spring 环境下可用
@Async+ 自定义TaskExecutor显式隔离
数据库/HTTP 客户端必须升级到支持虚拟线程的版本,否则会退化为平台线程
即使 Servlet 层用了虚拟线程,如果下游组件(如 JDBC 驱动、OkHttp、RestTemplate)内部仍使用 java.util.concurrent.ThreadPoolExecutor 做异步回调或连接复用,那么这些回调线程仍是平台线程,不仅浪费资源,还会因线程上下文切换破坏虚拟线程的调度连续性。
常见退化场景:
- HikariCP 默认连接池仍是平台线程驱动,需设置
com.zaxxer.hikari.HikariConfig#setConnectionInitSql配合虚拟线程感知的初始化逻辑(实际应换用支持 Loom 的 R2DBC 或新版 PgJDBC) - OkHttp 4.12+ 才通过
Dispatcher支持虚拟线程调度;旧版会 fallback 到AsyncTask平台线程 - Spring
RestTemplate不支持虚拟线程,必须改用WebClient(配合Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()))
日志和 MDC 上下文传递容易丢失,必须显式桥接
虚拟线程不是平台线程,ThreadLocal 中的数据(如 SLF4J 的 MDC、Spring 的 RequestContextHolder)不会自动继承。一个请求从 Tomcat 接收后进入虚拟线程,若 Controller 中打印日志,MDC 里的 traceId 可能为空。
根本原因:JVM 不自动复制 ThreadLocal 值到新挂载的载体线程上。
- SLF4J + Logback 场景:需启用
logback-classic1.5.0+,并配置<contextListener class="ch.qos.logback.classic.AsyncAppender"/>或使用MDC.putCopyOfContextMap()手动快照 - Spring 生态:确保使用 Spring Framework 6.1+,其
RequestContextHolder已支持虚拟线程上下文传播 - 自定义
ThreadLocal:必须在虚拟线程启动前调用threadLocal.get()读取,并在任务 lambda 内重新set()
最易被忽略的是跨虚拟线程的链路追踪:OpenTelemetry Java Agent 1.35+ 才完整支持虚拟线程上下文透传,低版本会丢失 span。

















