Executors.newVirtualThreadPerTaskExecutor()不是线程池,而是按需创建并自动回收虚拟线程的工厂,每个任务独占一个轻量级虚拟线程(约1KB栈),由JVM调度在少量载体线程上运行,无复用、无队列、无拒绝策略。

虚拟线程不是“池”,newVirtualThreadPerTaskExecutor() 本质是按需创建 + 自动回收
很多人看到“虚拟线程池”就下意识对标 Executors.newFixedThreadPool(200),这是第一个误区。Java 21 没有、也不该有“虚拟线程池”这个概念——newVirtualThreadPerTaskExecutor() 返回的 ExecutorService 并不维护任何可复用的线程实例,它只是为每个 submit() 或 execute() 创建一个新虚拟线程,任务结束即销毁,JVM 自动回收栈空间(约 1KB)。这和传统线程池“复用+阻塞队列+拒绝策略”的整套机制完全无关。
所以别纠结“池大小怎么配”。你提交 10 万个 IO 任务,就起 10 万个虚拟线程;它们共享默认 2~4 个平台线程(carrier threads),靠 JVM 调度器在挂起/恢复时动态切换上下文,不占 OS 线程资源。
Thread.sleep(100) 这类阻塞调用,在虚拟线程里不真“阻塞”平台线程
传统线程中 Thread.sleep()、Socket.read()、JDBC.query() 会让当前 OS 线程进入 WAITING 状态,无法干别的事。而虚拟线程执行同样代码时,JVM 检测到阻塞点,会立即把该虚拟线程从当前平台线程上卸载(unmount),让出 CPU 给其他就绪的虚拟线程——这个过程毫秒级,且无锁、无系统调用开销。
这意味着:
立即学习“Java免费学习笔记(深入)”;
- 同一平台线程可轮转调度成千上万个虚拟线程,只要它们大部分时间在等 IO
- 没有线程饥饿:哪怕有 1000 个任务同时卡在数据库响应上,剩下的 9000 个等待 HTTP 响应的任务仍能被快速调度
- 不用再写
CompletableFuture.supplyAsync(..., executor)套娃式异步链,同步写法照样高并发
CPU 密集型任务用虚拟线程反而更慢
虚拟线程的调度优势只在“频繁挂起 + 快速恢复”场景生效。如果任务纯计算、无 IO 阻塞(比如密集矩阵运算、JSON 解析大文件、循环哈希),虚拟线程会一直绑定在某个平台线程上跑满 CPU,导致其他虚拟线程无法被调度——此时它退化成“重量级 Thread 的廉价克隆”,但栈更小、创建快,实际吞吐反而不如固定大小的平台线程池。
判断标准很简单:看任务是否长期占用 CPU 而不交出控制权。如果是,必须用 Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 这类传统执行器。
Spring Boot 3.2 默认已启用虚拟线程支持,但需关掉 synchronized 和 ThreadLocal
Spring Boot 3.2+ 启动时自动注册 TaskExecutor 为虚拟线程驱动,HTTP 请求、@Scheduled、@Async 默认走虚拟线程。但两个常见陷阱会直接废掉性能:
-
synchronized块或方法会强制虚拟线程与平台线程绑定(pinning),失去卸载能力,变成“伪虚拟线程” -
ThreadLocal存储长生命周期对象(如 DB 连接、用户上下文)会导致内存泄漏——虚拟线程销毁后,其绑定的ThreadLocal值不会自动清理,而平台线程复用时会持续持有
替代方案:synchronized 改用 ReentrantLock 或无锁结构;ThreadLocal 改用 ScopedValue(JDK 21 新 API)或显式在 try-finally 中 remove()。



















