JAVA笔记之虚拟线程Virtual Thread
1. 什么是虚拟线程
虚拟线程(Virtual Threads) 是 JDK 21(JEP 444)正式提供的轻量级线程,由 JVM 调度,挂载在少量 载体线程(carrier / platform thread) 上。
| 维度 | 平台线程 | 虚拟线程 |
|---|---|---|
|
映射 |
1:1 OS 线程 |
多对少,JVM 调度 |
|
创建成本 |
高(~MB 栈、内核参与) |
低,可百万级 |
|
适用场景 |
CPU 密集、需固定线程亲和 |
I/O 密集、高并发阻塞代码 |
|
编程模型 |
传统 |
同步阻塞写法 + 高并发 |
适合: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 平台线程池 |
每个请求一个 虚拟线程 |
|
|
|
|
|
业务代码 |
同步阻塞 |
不变,仍是同步阻塞 |
|
单次请求 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)
| 指标 | 平台线程 (pool≈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 为主):
| 配置 | 吞吐 (req/s) | P99 |
|---|---|---|
|
平台线程池 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 密集(计算占主导)
| 配置 | 吞吐 (req/s) 示例 |
|---|---|
|
平台线程池 ≈ CPU 核数 (8) |
~12,000 |
|
虚拟线程 |
~9,800(更低) |
虚拟线程 不会让CPU 算得更快;并发超过核数还会增加调度开销。
混合负载(短 CPU + I/O)
500 并发示例:平台线程 ~1890 req/s → 虚拟线程 ~2680 req/s(约 +40%),不如纯 I/O 夸张,但通常仍有收益。
5. 与 goroutine的性能与模型对比
| 维度 | Java 虚拟线程 | Go goroutine |
|---|---|---|
|
调度者 |
JVM(ForkJoin 载体池) |
Go runtime(G-M-P) |
|
创建方式 |
|
|
|
语法风格 |
仍是 Java 线程 API |
语言级 |
|
阻塞 I/O |
unmount(未 pinning 时) |
网络 I/O 配合 netpoller,阻塞成本低 |
|
锁 |
|
无 JVM pinning;但锁竞争仍影响吞吐 |
|
跨「线程」传数据 |
共享堆、需注意可见性 |
推荐 channel;也可共享内存 |
|
单请求延迟 |
不会因虚拟线程变快 |
同样,协程不加速单次 CPU 计算 |
|
高并发 I/O 吞吐 |
相对平台线程显著提升 |
长期以高并发 I/O 著称 |
性能结论(经验性,非绝对):
- I/O 密集、同步代码:Java 21 + 虚拟线程 与 Go 在「可支撑的并发连接数 / 吞吐」上往往 同一量级,差距更多来自框架、GC、序列化、连接池配置,而非「协程 vs 虚拟线程」本身。
- CPU 密集:Go 的 goroutine 与 Java 虚拟线程 都不会魔法加速;Go 常略占优在调度轻量、无 pinning;Java 应用应把 CPU 工作放在 平台线程池 /
ForkJoinPool。 - 内存:两者都可支撑大量并发任务;Java 需注意 堆对象 + ThreadLocal + 线程栈(虚拟线程栈在堆上但仍占空间);Go 需注意 goroutine 栈增长与 channel 缓冲。
- 生态习惯:Go 默认
go+ channel;Java 传统是阻塞 + 线程,虚拟线程让 Spring 式同步代码 无需全面 Reactive 即可获得类似扩展性。
6. 各流行 Java 框架对虚拟线程的支持
| 框架 / 组件 | 支持情况 | 启用方式 / 说明 |
|---|---|---|
|
JDK 21+ |
正式(JEP 444) |
无需预览开关 |
|
Spring Boot 3.2+ |
一等公民,全局开关 |
|
|
Spring Framework 6.1+ |
与 Boot 配套 |
|
|
Spring WebFlux |
已是 Reactor 非阻塞 |
虚拟线程主要惠及 WebMVC(阻塞);WebFlux 不必为了并发强行切 |
|
Quarkus 3.x |
显式、按方法 |
|
|
Micronaut 4.x |
注解 + 实验特性 |
|
|
Tomcat 10.1+ |
支持 |
Spring Boot 开启后自动配置 |
|
Netty |
4.2+ 对 pinning 有改进 |
常作底层;业务阻塞代码放虚拟线程 |
|
Hibernate / JDBC |
阻塞 API 可用 |
注意连接池大小与 pinning;用较新驱动 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)