1. 什么是虚拟线程
虚拟线程(Virtual Threads) 是 JDK 21(JEP 444)正式提供的轻量级线程,由 JVM 调度,挂载在少量 载体线程(carrier / platform thread) 上。
|
映射 |
1:1 OS 线程 |
多对少,JVM 调度 |
|
创建成本 |
高(~MB 栈、内核参与) |
低,可百万级 |
|
适用场景 |
CPU 密集、需固定线程亲和 |
I/O 密集、高并发阻塞代码 |
|
编程模型 |
传统 Thread |
同步阻塞写法 + 高并发 |
适合:HTTP/RPC、JDBC、文件/网络阻塞 I/O。
不适合:长时间 CPU 计算(应交给 ForkJoinPool / 专用池)、大量 synchronized + 阻塞 I/O(pinning)、依赖 ThreadLocal 大量泄漏的场景。
2. 核心 API 与代码示例
2.1 创建
// Java 21+
Thread vThread = Thread.ofVirtual().name("worker-", 0).start(() -> {
System.out.println("on virtual thread: " + Thread.currentThread());
});
// 未 start,手动 start
Thread vt = Thread.ofVirtual().name("api-call").unstarted(() -> callRemote());
vt.start();
vt.join();
// 工厂:批量命名
Thread.Builder builder = Thread.ofVirtual().name("pool-", 0);
2.2 ExecutorService
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
String body = httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
return body.length();
})
);
} // try-with-resources 会 shutdown 并等待
每个任务一个虚拟线程,无需像传统线程池那样调 corePoolSize/maxPoolSize 来扛并发。
2.3 Thread.startVirtualThread
Thread.startVirtualThread(() -> {
try (var in = Files.newInputStream(path)) {
// 阻塞 I/O 在虚拟线程上通常没问题
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
2.4 从平台线程切到虚拟线程
ExecutorService virtualExec = Executors.newVirtualThreadPerTaskExecutor();
virtualExec.submit(() -> {
// 此处 Thread.currentThread().isVirtual() == true
jdbcTemplate.query("SELECT …", rs -> { /* 阻塞 */ });
});
2.5 StructuredTaskScope
并行子任务、失败时取消兄弟任务:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> user = scope.fork(() -> fetchUser(id));
Subtask<String> order = scope.fork(() -> fetchOrder(id));
scope.join(); // 等待全部
scope.throwIfFailed(); // 任一失败则传播
return user.get() + order.get();
}
适合「一个请求里多个下游调用」;注意 API 在 JDK 22/23 有包名与类型调整,以你使用的 JDK 文档为准。
2.6 与 CompletableFuture 配合
ExecutorService vtExec = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> callA(), vtExec);
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> callB(), vtExec);
String result = a.thenCombine(b, (x, y) -> x + y).join();
要点:异步链路的 执行器 要显式指定虚拟线程执行器,否则默认可能仍用 ForkJoinPool.commonPool()(平台线程)。
2.7 检测与调试
Thread t = Thread.currentThread();
t.isVirtual(); // true / false
// 启动参数:发现 pinning
// -Djdk.tracePinnedThreads=short
3. 使用时注意事项
3.1 Pinning(载体线程被占住)
在虚拟线程上进入 synchronized 或 native 方法里的阻塞 时,载体线程无法去跑别的虚拟线程,并发能力会掉下去。
// 风险:synchronized 块内有阻塞 I/O
synchronized (lock) {
socket.getInputStream().read(); // 可能 pin 住 carrier
}
// 更稳妥:ReentrantLock + 可中断/限时,或换无锁/分段锁
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 仍要避免长时间阻塞;优先用虚拟线程友好的库
} finally {
lock.unlock();
}
排查:-Djdk.tracePinnedThreads=short,并升级 HikariCP 5+、HttpClient 5、较新 JDBC/驱动 等已减少 pinning 的组件。
3.2 线程池 ≠ 连接池
虚拟线程可以很多,但 数据库/Redis/下游 HTTP 连接池 仍受下游限制。应按下游容量设 maximumPoolSize,而不是按虚拟线程数量设。
3.3 ThreadLocal
每个虚拟线程仍可有 ThreadLocal;百万虚拟线程 + 大对象 ThreadLocal = 内存风险。框架里大量隐式 ThreadLocal(如部分老 MVC/日志上下文)要审计。
3.4 不要池化虚拟线程
官方建议:每个任务 newVirtualThreadPerTaskExecutor(),不要搞「虚拟线程池大小 = 200」这种传统池思维(除非特殊场景用 Semaphore 限流)。
3.5 Thread.sleep / 阻塞 I/O
虚拟线程在 sleep、NIO、BlockingQueue.take 等会 unmount,载体可复用——这是相对平台线程的核心优势。
4. Spring Boot 启用虚拟线程前后对比
4.1 开关前后,运行时到底变了什么
|
请求线程 |
Tomcat/Jetty 平台线程池(如 max 200) |
每个请求一个 虚拟线程 |
|
@Async / Task 执行 |
ThreadPoolTaskExecutor(有界池) |
SimpleAsyncTaskExecutor(每任务虚拟线程) |
|
业务代码 |
同步阻塞 |
不变,仍是同步阻塞 |
|
单次请求 CPU 时间 |
不变 |
不变 |
|
瓶颈 |
线程池满 → 排队 |
更常变成 DB/HTTP 连接池、下游、GC |
前提:Java 21+、Spring Boot 3.2+(Spring Framework 6.1+)。
spring.threads.virtual.enabled=true
4.2 吞吐量:按并发度分三档(典型 I/O 型 REST)
场景多为:Controller → JDBC/HTTP 下游,单次请求里有 几十~几百 ms 阻塞等待(社区压测常见设定,非官方基准)。
低并发(约 100 并发用户)
|
吞吐 (req/s) |
~950 |
~980 |
约 +0~5% |
|
P95 延迟 |
~105 ms |
~102 ms |
几乎一样 |
原因:线程池还没打满,平台线程够用,虚拟线程优势体现不出来。
中高并发(约 500~1000,池大小 ~200)
|
吞吐 (req/s) |
~1,240~1,890 |
~2,650~3,850 |
约 +40%~2.3× |
|
平均延迟 |
~264~805 ms |
~55~190 ms |
明显下降 |
|
P99 |
~430~1,450 ms |
~92~310 ms |
明显下降 |
原因:平台线程池 排队;虚拟线程在阻塞 I/O 时 unmount,载体可接更多请求,吞吐不再被「200 个 OS 线程」卡死。
另一组相近的 MVC 对比(4 vCPU、I/O 为主):
|
平台线程池 200 |
~1,850 |
~420 ms |
|
虚拟线程 |
~4,200 |
~180 ms |
|
WebFlux(参考) |
~4,350 |
~175 ms |
结论:I/O 密集的 Spring MVC,虚拟线程吞吐可接近 WebFlux,代码仍是阻塞风格。
极高并发(5000+ 并发 / 万级连接)
|
吞吐 |
随排队 崩溃或持平很低 |
常仍能维持 数千~近万 req/s(视下游而定) |
|
极端情况 |
线程过多 → OOM / 拒绝 |
仍可能跑,但受 连接池、DB、网络 限制 |
有压测报告:1000 并发时平台线程 ~980 req/s、虚拟线程 ~3850 req/s(约 4×);10000 并发时平台线程失败、虚拟线程仍 ~9650 req/s——这类数字 强依赖 机器、池配置、是否 pinning,只能作量级参考。
4.3 什么时候吞吐几乎不变甚至变差
CPU 密集(计算占主导)
|
平台线程池 ≈ CPU 核数 (8) |
~12,000 |
|
虚拟线程 |
~9,800(更低) |
虚拟线程 不会 让 CPU 算得更快;并发超过核数还会增加调度开销。
混合负载(短 CPU + I/O)
500 并发示例:平台线程 ~1890 req/s → 虚拟线程 ~2680 req/s(约 +40%),不如纯 I/O 夸张,但通常仍有收益。
5. 与 goroutine的性能与模型对比
|
调度者 |
JVM(ForkJoin 载体池) |
Go runtime(G-M-P) |
|
创建方式 |
Thread.ofVirtual() / Executor |
go func() |
|
语法风格 |
仍是 Java 线程 API |
语言级 go |
|
阻塞 I/O |
unmount(未 pinning 时) |
网络 I/O 常配合 netpoller,阻塞成本低 |
|
锁 |
synchronized 有 pinning 风险 |
无 JVM pinning;但锁竞争仍影响吞吐 |
|
跨「线程」传数据 |
共享堆、需注意可见性 |
常推荐 channel;也可共享内存 |
|
单请求延迟 |
不会因虚拟线程变快 |
同样,协程不加速单次 CPU 计算 |
|
高并发 I/O 吞吐 |
相对平台线程显著提升 |
长期以高并发 I/O 著称 |
性能结论(经验性,非绝对):
6. 各流行 Java 框架对虚拟线程的支持
|
JDK 21+ |
正式(JEP 444) |
无需预览开关 |
|
Spring Boot 3.2+ |
一等公民,全局开关 |
spring.threads.virtual.enabled=true(需 Java 21+);Tomcat/Jetty 请求、@Async、Task 执行/调度默认走虚拟线程 |
|
Spring Framework 6.1+ |
与 Boot 配套 |
TaskExecutor / SimpleAsyncTaskExecutor 虚拟线程变体 |
|
Spring WebFlux |
已是 Reactor 非阻塞 |
虚拟线程主要惠及 WebMVC(阻塞);WebFlux 不必为了并发强行切 |
|
Quarkus 3.x |
显式、按方法 |
@RunOnVirtualThread;扩展 quarkus-virtual-threads;底层仍是 Vert.x/Netty,虚拟线程是「跑阻塞代码」的叠加层 |
|
Micronaut 4.x |
注解 + 实验特性 |
@ExecuteOn(TaskExecutors.VIRTUAL);4.9+ Loom carrier(实验)把 Netty 事件循环与载体结合 |
|
Helidon 4 |
SE 以虚拟线程为重要路径 |
与 Quarkus/Micronaut 不同,更偏「虚拟线程优先」架构(具体以官方文档为准) |
|
Jakarta EE / Payara 6.2024+ |
逐步支持 |
企业版需看厂商发行说明 |
|
Tomcat 10.1+ |
支持 Executor 用虚拟线程 |
Spring Boot 开启后自动配置 |
|
Jetty 12 |
支持 |
同左 |
|
Undertow |
视版本 / Quarkus 集成 |
随 Quarkus 文档 |
|
Netty |
4.2+ 对 pinning 有改进 |
常作底层;业务阻塞代码放虚拟线程 |
|
Hibernate / JDBC |
阻塞 API 可用 |
注意连接池大小与 pinning;用较新驱动 |
|
Apache HttpClient 4 |
老版本易 pinning |
优先 HttpClient 5 |
|
Vert.x / Mutiny |
Quarkus 推荐 await 等 |
减少在虚拟线程里裸 synchronized 阻塞 |



