虚拟线程
约 1029 字大约 3 分钟
布欧-Lewyon
2026-05-16
虚拟线程(Virtual Threads)是 JDK 21 正式发布的特性(Project Loom 的产物)。它是一种轻量级线程,由 JVM 管理而非操作系统,让 Java 应用可以用"每请求一线程"的简单模型处理海量并发,而无需依赖异步回调或响应式编程。
平台线程 vs 虚拟线程
| 对比维度 | 平台线程(Platform Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 管理方 | 操作系统内核 | JVM |
| 创建成本 | 高(MB 级栈空间) | 低(KB 级栈空间,可数百万) |
| 上下文切换 | 内核调度,昂贵 | JVM 调度,轻量 |
| 适用场景 | CPU 密集型、长期运行 | IO 密集型(大量阻塞等待) |
| JDK 版本 | 始终存在 | JDK 21 正式版(预览版在 JDK 19/20) |
创建虚拟线程
// 方式一:Thread.ofVirtual(JDK 21+)
Thread vThread = Thread.ofVirtual()
.name("vt-1")
.start(() -> {
System.out.println("虚拟线程: " + Thread.currentThread());
});
// 方式二:Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
// 每个任务在一个新虚拟线程中执行
Thread.sleep(100); // 不会阻塞平台线程
return compute();
});
}
}对比:平台线程池 vs 虚拟线程
// 传统方式:固定线程池 — 线程数受限于池大小
try (var executor = Executors.newFixedThreadPool(200)) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 200 个任务同时执行,其他排队
return null;
});
}
}
// 虚拟线程方式:每任务一线程 — 无需池
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 所有任务"同时"执行,每个虚拟线程挂起时不阻塞平台线程
return null;
});
}
}工作原理
虚拟线程由 JVM 调度运行在少量的平台线程(称为 Carrier Thread,默认数量与 CPU 核数相同)上:
虚拟线程 A ─┐
虚拟线程 B ─┼── 平台线程 1(Carrier)
虚拟线程 C ─┘
┌── 平台线程 2(Carrier)
虚拟线程 D ───┘当虚拟线程执行到阻塞操作(如 Thread.sleep、IO 读写、锁等待)时,JVM 会将其挂起并卸载(unmount)到堆上,释放 Carrier 线程去执行其他虚拟线程。IO 完成后重新挂载(mount)到 Carrier 上继续执行。
适用场景
IO 密集型(最理想):Web 服务器、API 网关、数据库访问、RPC 调用——大量时间花在等待外部响应上。
不适用的场景:
- CPU 密集型(计算 π、视频编码、加密解密):虚拟线程和平台线程没区别。
synchronized块内持有锁时阻塞:虚拟线程在synchronized块内阻塞会钉住(pinned)其 Carrier 线程,失去轻量优势。- JNI 调用:本地方法调用期间无法卸载。
锁使用建议
// 可能钉住 Carrier(synchronized 块内阻塞)
synchronized (lock) {
Thread.sleep(1000);
}
// 推荐:用 ReentrantLock 替代 synchronized 以避免钉住
private final Lock lock = new ReentrantLock();
lock.lock();
try {
Thread.sleep(1000);
} finally {
lock.unlock();
}结构化并发(预览)
JDK 21 还引入了 StructuredTaskScope(预览 API),让并发任务的生命周期与代码结构对齐:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser());
Future<Order> order = scope.fork(() -> fetchOrder());
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 任一失败则抛出
return new Response(user.resultNow(), order.resultNow());
}小结
- 虚拟线程是轻量级线程,创建成本极低,适合IO 密集型的海量并发场景。
- 虚拟线程底层运行在少量平台线程上,阻塞时自动挂载/卸载,不浪费 OS 资源。
- 推荐用
Executors.newVirtualThreadPerTaskExecutor()创建,替代传统的线程池模型。 - 避免在
synchronized块中执行阻塞操作(会钉住 Carrier),优先用ReentrantLock。 - 易错:虚拟线程不是用来替代线程池的——它们是互补的。CPU 密集型任务和长期稳定连接仍用平台线程池。另外,
ThreadLocal在虚拟线程中虽然可用,但创建数百万个虚拟线程时,每个都携带 ThreadLocal 对象可能导致内存压力。 - 思考任务:创建一个对比实验:用固定线程池(10 线程)和虚拟线程分别执行 500 个
Thread.sleep(100)任务,对比总完成时间。
上一节:JUC 工具类
下一节:并发问题与排查
