虚拟线程(Virtual Threads)是 JDK 21 中最具革命性的特性,它将 Java 的并发模型从“一对一”(1 个 Java 线程对应 1 个 OS 线程)转变为“多对多”(M 个虚拟线程映射到 N 个平台线程)。
以下是关于虚拟线程的用法、适用场景以及关键注意事项的详细指南。
1. 核心用法 (Usage)
虚拟线程的使用非常简单,几乎不需要学习新的 API,主要基于标准的 java.lang.Thread 和 java.util.concurrent.Executors。
A. 创建方式
方式一:使用 Thread 工厂方法(推荐用于独立任务)
// 创建一个虚拟线程并启动
Thread virtualThread = Thread.ofVirtual().name("my-virtual-thread", 0) // 可选:设置名称前缀.start(() -> {System.out.println("Running in: " + Thread.currentThread());// 模拟 I/O 操作try { Thread.sleep(1000); } catch (InterruptedException e) {}});// 等待完成
virtualThread.join();
方式二:使用 Executors 线程池(推荐用于批量任务)
这是最常用的方式,可以像使用传统线程池一样管理数百万个虚拟线程。
// 创建一个为每个任务生成新虚拟线程的执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {for (int i = 0; i < 1_000_000; i++) {final int taskId = i;executor.submit(() -> {// 模拟网络请求或数据库查询simulateIoTask(taskId); });}
} // try-with-resources 会自动等待所有任务完成并关闭
方式三:在现有代码中无缝替换
如果你有一个现有的线程池配置,只需将 Executors.newFixedThreadPool(...) 替换为 Executors.newVirtualThreadPerTaskExecutor(),通常无需修改业务逻辑代码。
B. 与 Platform Thread 的区别
- 默认行为:通过
Thread.start()启动的是平台线程(Platform Thread,即传统的 OS 线程)。 - 虚拟线程标识:可以通过
thread.isVirtual()判断是否为虚拟线程。 - 堆栈跟踪:虚拟线程的堆栈存储在 Java 堆中,而不是操作系统栈中,因此可以动态调整大小。
2. 最佳使用场景 (Use Cases)
虚拟线程的设计初衷是解决 I/O 密集型 任务的高并发问题。
✅ 强烈推荐场景
-
高并发网络服务:
- Web 服务器(如 Tomcat, Jetty, Spring Boot 3.2+ 已默认支持虚拟线程)。
- 处理大量 HTTP 请求、WebSocket 连接、gRPC 调用。
- 优势:每个请求一个线程(Thread-per-Request),代码同步阻塞但性能极高,不再需要复杂的 Reactor 模式(如 WebFlux)。
-
数据库交互密集的应用:
- 应用需要频繁等待数据库响应。
- 优势:当线程在执行 JDBC 调用阻塞时,JVM 会自动“卸载”(unmount)该虚拟线程,释放底层的平台线程去处理其他任务。
-
调用外部 API / 微服务编排:
- 需要并行调用多个下游服务并等待结果。
- 优势:可以轻松发起成千上万个并行调用而不会耗尽系统资源。
-
定时任务与延迟处理:
- 大量的
Thread.sleep()或延迟队列处理。
- 大量的
❌ 不推荐/无效场景
-
CPU 密集型任务:
- 如:图像渲染、复杂数学计算、视频编码、大规模数据排序。
- 原因:虚拟线程无法增加 CPU 核心数。如果任务是纯计算的,虚拟线程只会增加上下文切换的开销,性能甚至不如平台线程。此时应继续使用
newFixedThreadPool限制线程数为 CPU 核数。
-
持有锁时间过长的场景:
- 如果代码中包含长时间的
synchronized块或非弹性锁(见注意事项),会导致底层平台线程被阻塞,降低吞吐量。
- 如果代码中包含长时间的
3. 关键注意事项与陷阱 (Cautions & Pitfalls)
虽然虚拟线程很强大,但 misuse(误用)可能导致严重的性能下降。
⚠️ 1. 避免“固定线程池”抗模式 (The Fixed Thread Pool Anti-Pattern)
错误做法:
// 千万不要这样做!
ExecutorService executor = Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());
原因:虚拟线程极其轻量,不需要限制数量。限制虚拟线程池的大小会人为地制造瓶颈,导致任务排队,失去了虚拟线程“按需创建”的优势。
正确做法:始终使用 Executors.newVirtualThreadPerTaskExecutor(),它会在每个任务提交时创建一个新的虚拟线程,任务结束后自动销毁。
⚠️ 2. 警惕“阻塞”平台线程的代码 (Pinning)
虚拟线程在执行阻塞操作(如 I/O、sleep)时,会从平台线程上“卸载”下来。但是,某些操作会导致虚拟线程** pinned **(绑定)到平台线程上,无法卸载。如果此时有大量虚拟线程被 pinned,会耗尽平台线程,导致系统停滞。
导致 Pinned 的主要原因:
- 在
synchronized块中执行阻塞操作:
解决方案:将// 危险:如果在 synchronized 块内调用 network.read(),虚拟线程会被 pinned synchronized(lock) {inputStream.read(); // 阻塞操作 }synchronized替换为ReentrantLock(Java 21 中的ReentrantLock已优化,支持虚拟线程卸载)。 - Native 方法调用:某些 JNI 调用可能会强制绑定。
⚠️ 3. 线程局部变量 (ThreadLocal) 的滥用
- 问题:在传统模型中,线程池复用线程,
ThreadLocal是安全的。但在虚拟线程模型中,可能会创建数百万个虚拟线程。如果每个虚拟线程都设置大量的ThreadLocal变量,会导致巨大的内存开销(尽管比平台线程小,但量变引起质变)。 - 建议:尽量减少
ThreadLocal的使用,或者确保在使用后及时remove()。对于上下文传递,考虑使用显式参数传递或协程式的上下文对象。
⚠️ 4. 调试与监控的变化
- 堆栈跟踪:由于虚拟线程数量巨大,传统的 dump 文件(Thread Dump)可能会变得非常大且难以阅读。
- 工具支持:确保你的监控工具(如 Prometheus, Grafana, APM 工具)和 IDE 已更新以支持 JDK 21 的虚拟线程视图。JDK 21 提供了新的
jcmd选项来专门查看虚拟线程状态。
⚠️ 5. 兼容性
- 库的支持:大多数主流框架(Spring Boot 3.2+, Micronaut, Quarkus, Tomcat 10.1+, Jetty 12+)已完美支持。但一些老旧的第三方库如果重度依赖
ThreadLocal或假设线程数量很少,可能会出现意外行为。
总结建议
| 维度 | 建议 |
|---|---|
| 何时使用 | I/O 密集型、高并发网络服务、微服务调用、数据库访问。 |
| 何时不用 | 纯 CPU 计算任务、需要严格限制并发数的场景。 |
| 创建方式 | 使用 Executors.newVirtualThreadPerTaskExecutor()。 |
| 最大禁忌 | 不要将虚拟线程放入固定大小的线程池中;避免在 synchronized 块中进行阻塞 I/O。 |
| 锁的选择 | 优先使用 java.util.concurrent.locks.ReentrantLock 代替 synchronized。 |
通过正确使用虚拟线程,你可以用简单的同步代码风格(Blocking Style)实现以前只有复杂的异步响应式编程(Reactive Style)才能达到的高吞吐量,极大地降低了开发和维护成本。
