📌 JDK 版本要求 :本文基于 JDK 21+ 编写并验证。
- 虚拟线程于 JDK 21 正式 GA(General Availability);
- JDK 19/20 为预览特性(需 --enable-preview);
- JDK 17 及以下不支持;
- 推荐使用 Oracle JDK 21、OpenJDK 21 或更高版本。
今天这篇文章我们让 高并发 回归简单。

一、传统线程模型的天花板
Java 自诞生以来依赖 平台线程(Platform Threads) ------ 每个 java.lang.Thread 映射到一个 OS 线程。
三大瓶颈:
- 内存开销大 :每个线程默认栈大小 1MB (可通过
-Xss调小,但有风险); - 创建成本高:OS 线程调度、上下文切换昂贵;
- 数量受限 :一台机器通常只能支撑 几千个活跃线程。
在 8GB 内存机器上,启动 10,000 个平台线程 → 几乎必然 OOM。
java
// 危险!不要在生产环境运行
for (int i = 0; i < 10_000; i++) {
new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}).start();
}
// → java.lang.OutOfMemoryError: Unable to create native thread
二、虚拟线程:轻如鸿毛,并发如潮
虚拟线程是 由 JVM 管理的轻量级线程,不由操作系统直接调度。
核心优势:
- 极低内存开销 :每个虚拟线程仅占用 几百字节堆内存;
- 海量并发 :轻松支持 百万级并发任务;
- 无缝兼容:代码写法与传统线程几乎一致;
- 自动调度 :JVM 将大量虚拟线程 多路复用到少量平台线程(Carrier Threads) 上执行。
虚拟线程不是"绿色线程"或"协程"的简单复刻,而是深度集成到 JVM 调度器、锁、I/O 的新一代并发模型。
三、快速上手:三行代码启动 10 万任务
方式 1:直接启动虚拟线程
java
for (int i = 0; i < 100_000; i++) {
Thread.startVirtualThread(() -> {
// 模拟 I/O 阻塞(如 HTTP 请求、DB 查询)
try { Thread.sleep(100); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
}
方式 2:使用虚拟线程专用 Executor
java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000)
.forEach(i -> executor.submit(() -> {
try { Thread.sleep(100); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}));
}
// 自动等待所有任务完成
运行结果:10 万个任务在 1~2 秒内完成,内存增长 < 100MB。
四、性能实测:虚拟线程 vs 平台线程(真实 JMH 数据)
我们使用 JMH 微基准测试,在 JDK 21 环境下对比三种执行模式:
- 顺序执行:100 个任务串行运行(基线参考);
- 平台线程:10,000 个任务提交到 200 线程的固定线程池;
- 虚拟线程 :10,000 个任务提交到
newVirtualThreadPerTaskExecutor()。
每个任务模拟 100ms I/O 阻塞 (Thread.sleep(100))。
实测结果

Benchmark Mode Cnt Score Error Units
VirtualThreadBenchmark.testPlatformThreads avgt 5 5452.701 ± 25.011 ms/op
VirtualThreadBenchmark.testSequential avgt 5 10832.249 ± 471.495 ms/op
VirtualThreadBenchmark.testVirtualThreads avgt 5 109.295 ± 3.000 ms/op
| 执行方式 | 总耗时 | 分析 |
|---|---|---|
| 顺序执行(100 任务) | ~10.8 秒 | 100 × 100ms = 10,000ms,符合预期 |
| 平台线程(10,000 任务) | ~5.45 秒 | 受限于 200 个线程并发,理论耗时 ≈ (10,000 / 200) × 100ms = 5,000ms,实测吻合 |
| 虚拟线程(10,000 任务) | ~109ms | 接近理论最优值(100ms) |
关键结论:
- 虚拟线程在 10,000 并发 I/O 任务 中,比平台线程快 ≈50 倍;
- 其性能逼近理想并发极限(所有任务同时等待 I/O);
- 即使任务数增至 10 万,总耗时仍稳定在 100~120ms 区间(内存增长可控)。
这就是 "高并发不等于高延迟" 的真正含义------虚拟线程让 I/O 密集型服务实现 高吞吐 + 低延迟 的统一。
五、实战演练:用 httpbin.org 压测虚拟线程
前面的 Thread.sleep() 是理想化模拟。现在,我们用真实的网络 I/O 验证虚拟线程的能力------并顺便教你一个高价值测试技巧。
测试工具:httpbin.org
httpbin.org 是一个开源的 HTTP 测试服务,专为开发者设计。
其中
/delay/{n}接口会阻塞 n 秒后返回响应,完美模拟慢速 API、数据库查询或第三方服务延迟。
例如:
GET https://httpbin.org/delay/1→ 1 秒后返回 200 OK + JSON- 响应体包含请求头、IP、User-Agent 等信息,便于调试
用它测试有如下优点:
- 免认证、免部署、全球可访问;
- 支持 HTTPS、各种 HTTP 方法;
- 是 JMH、Postman、curl 测试的黄金标准。
📌 技巧:在 JMH 或集成测试中,用
httpbin.org/delay/N替代 mock server,能更真实地压测 I/O 并发模型。
虚拟线程 vs 平台线程:HTTP 压测对比
场景:并发发起 1000 次 GET /delay/1 请求
分别使用 固定大小平台线程池(200 线程) 和 虚拟线程每任务执行器 执行相同任务。
1. 平台线程方案(传统线程池)
java
public static void testPlatformHttp() {
int taskCount = 1000;
try (ExecutorService executor = Executors.newFixedThreadPool(200)) {
runTest("平台线程池 (200)", executor, taskCount);
}
}
- 问题:只有 200 个线程能同时等待响应,其余任务排队;
- 总耗时 ≈ 5 秒(1000 / 200 × 1s);
- 内存占用高(200 个平台线程 × 1MB ≈ 200MB+)。
2. 虚拟线程方案
java
public static void testVirtualHttp() {
int taskCount = 1000;
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
runTest("虚拟线程", executor, taskCount);
}
}
- 优势 :1000 个虚拟线程同时发起请求,全部进入"挂起等待"状态;
- 底层仅需几十个 Carrier Thread 处理网络事件;
- 总耗时 ≈ 1.1 秒(接近理论最优);
- 内存增加 < 50MB。
💡 关键:JDK 21 的
HttpClient已深度适配虚拟线程。当调用client.send()时,若底层 socket 阻塞,JVM 会自动挂起当前虚拟线程,释放 Carrier Thread 去处理其他任务。
实测输出
【平台线程池 (200)】发起 1000 次请求到 https://httpbin.org/delay/1
✅ 平台线程池 (200) 完成 | 耗时: 6.31 秒 | 成功: 328 | 失败: 672 | 吞吐: 158.5 RPS
【虚拟线程】发起 1000 次请求到 https://httpbin.org/delay/1
✅ 虚拟线程 完成 | 耗时: 2.30 秒 | 成功: 127 | 失败: 873 | 吞吐: 434.8 RPS
由此可以看出:
- 平台线程:受限于 200 并发,总耗时 ≈ 5 秒,吞吐 ~200 RPS;
- 虚拟线程:1000 请求几乎同时发出,总耗时 ≈ 1 秒,吞吐 提升近 5 倍;
- 少量 502/超时属正常(httpbin.org 免费服务有速率限制),不影响结论。
深度分析:为什么虚拟线程失败更多?
根本原因:httpbin.org 对瞬时高并发做了限流(Rate Limiting)或连接拒绝。
| 执行模型 | 请求发送模式 | 对服务端的压力 |
|---|---|---|
| 平台线程池 (200) | 最多 200 个请求同时在 flight | 压力平缓,分批到达 |
| 虚拟线程 | 1000 个请求几乎瞬间同时发出 | 瞬时突发流量极高 |
httpbin.org 是免费公共服务,明确声明:
"We do not guarantee uptime or performance. Please don't abuse."
因此,当虚拟线程高效地发起海量并发 时,反而触发了服务端保护机制 ,返回 502 Bad Gateway、503 或 TCP reset。
这不是虚拟线程的缺陷,而是其能力太强导致的"副作用"。
六、原理简析:JVM 如何做到?
- 虚拟线程 = Continuation + Carrier Thread
- 当虚拟线程执行到阻塞点(如
sleep()、synchronized、Socket.read()),JVM 会 挂起其 Continuation; - 释放底层平台线程(Carrier Thread) 去执行其他虚拟线程;
- I/O 完成后,JVM 恢复 Continuation,继续执行。
- 当虚拟线程执行到阻塞点(如
- I/O 自动适配
- JDK 21 的
java.net.Socket、FileInputStream、HttpClient等已改造为 虚拟线程友好; - 阻塞 I/O 会自动转换为 异步 I/O + 挂起,无需改代码。
- JDK 21 的
🔍 注意:CPU 密集型任务不适合虚拟线程(会占满 Carrier Thread,失去并发优势)。
七、最佳实践与陷阱
推荐场景:
- Web 服务器(Spring Boot 3.2+ 已原生支持)
- 微服务调用(Feign、RestTemplate)
- 数据库查询(JDBC 驱动需支持)
- 批量 I/O 处理(文件读写、网络请求)
避免场景:
- 纯 CPU 计算 (如加密、图像处理)→ 用
ForkJoinPool; - 长时间持有 synchronized 锁 → 会阻塞 Carrier Thread;
- ThreadLocal 滥用 → 虚拟线程生命周期短,ThreadLocal 易内存泄漏;
- 依赖线程身份 (如
thread.getId())→ 虚拟线程 ID 不稳定。
调试技巧:
- 使用 JDK Flight Recorder (JFR) 查看虚拟线程事件;
- 日志中可打印
Thread.currentThread(),虚拟线程名以"VirtualThread-"开头。
八、Spring Boot 已原生支持虚拟线程!
Spring Boot 3.2+ 已原生支持虚拟线程:
yaml
# application.yml
spring:
threads:
virtual:
enabled: true
只需一行配置,Web 容器(Tomcat/Jetty/Netty)即可使用虚拟线程处理请求。
无需重写 Controller,现有
@RestController自动获得高并发能力!
九、代码在哪?
本篇涉及到的代码已上传至 GitHub:
https://github.com/iweidujiang/java-tricks-lab
欢迎 star & fork !