虚拟线程常见问题总结¶
虚拟线程(Virtual Threads)是 Java 21 正式引入的“JVM 级轻量线程”,主要用来让大量阻塞型 IO 代码写起来还是同步风格,但能像异步框架一样支撑超高并发。
1. 虚拟线程到底是什么、和平台线程是什么关系?¶
- 虚拟线程:由 JVM 而不是操作系统创建和调度的轻量级线程,数量可以非常大(成万上百万级),专门跑你的应用代码。
- 平台线程(platform thread):JVM 里的“普通线程”,一对一映射到底层 OS 线程(之前我们用的 Thread 基本都是这个)。
- OS线程(Operating System Thread)指操作系统(如Linux、Windows)内核创建和调度的线程,每个OS线程拥有独立的栈、寄存器和调度状态,由内核负责上下文切换和资源分配。
- OS线程是操作系统内核直接管理的执行单元,是CPU调度的基本单位。
- 关系模型:多个虚拟线程在不同时间“挂载”到少量平台线程上执行,阻塞时会“卸载”平台线程,让平台线程去服务其他虚拟线程。
直观理解:平台线程像“车”,虚拟线程像“乘客任务”。以前每个任务一辆车(平台线程),现在是很多任务轮流坐少量的车(虚拟线程调度)。
2. 虚拟线程适合/不适合什么场景?¶
适合:大量阻塞型 IO
- 典型场景:Web 服务高并发请求、RPC 调用下游、访问 DB/Redis/外部 HTTP 接口、消息消费里大量等待网络 IO 等。
- 优点:
- 可以为“每个请求 / 每个任务”创建一个虚拟线程而不用担心线程数爆炸。
- 阻塞时会让出平台线程,几乎不浪费 OS 线程资源;更少的上下文切换、更好的吞吐。
- 代码仍是同步风格,不必强行改造成回调/响应式链式调用,降低心智负担。
不适合:计算密集型任务
- 对 CPU 密集型任务(加解密、复杂计算、图像处理等),虚拟线程不会提升性能,因为瓶颈在 CPU,不在线程调度。
- 这类任务仍然推荐用固定大小线程池(平台线程),线程数控制在 CPU 核数附近即可。
3. 虚拟线程如何创建与使用?¶
Java 21 提供几种标准用法(推荐你记住后两种):
1)直接启动一个虚拟线程:
public class VirtualThreadDemo {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Run in virtual thread: " + Thread.currentThread());
};
Thread.startVirtualThread(task);
}
}
2)通过 Thread.ofVirtual():
public class VirtualThreadDemo {
public static void main(String[] args) {
Runnable task = () -> System.out.println(Thread.currentThread());
// 创建但不启动
Thread t = Thread.ofVirtual().unstarted(task);
t.start();
// 创建即启动
Thread.ofVirtual().start(task);
}
}
3)通过 ThreadFactory:
ThreadFactory factory = Thread.ofVirtual().factory();
Thread vt = factory.newThread(() -> System.out.println(Thread.currentThread()));
vt.start();
4)每任务一个虚拟线程执行器(最常用):
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
// 模拟IO
Thread.sleep(500);
return "ok";
});
}
}
要点:newVirtualThreadPerTaskExecutor() 适合 IO 密集型场景,“一个任务一个虚拟线程”,不再需要自己精细配置 corePoolSize/maximumPoolSize。
4. 常见问题与“坑点”¶
问题一:虚拟线程会不会把内存打爆?
- 虚拟线程自身栈非常小,由 JVM 按需增长,创建成本远小于平台线程,但不是“没有成本”。
- 大量虚拟线程仍然会带来:栈空间占用、任务对象、ThreadLocal 内容等内存消耗,如果每个虚拟线程里存放大对象,GC 压力会很大。
- 实战建议:
- 避免在虚拟线程里大量使用 ThreadLocal 存放大对象(尤其是缓存、上下文等)。
- 通过压测观察堆占用、GC 情况,控制同时活跃的任务数量。
问题二:和 synchronized、锁配合会怎样?
- 虚拟线程在 synchronized 块内阻塞时,JDK 会尽量让出平台线程(Java 24 引入 JEP 491 进一步优化这点),避免长时间“固定 (pinning)”平台线程。
- 但目前仍有场景会“固定”:
- synchronized 里做阻塞 IO 或调用某些本地方法时,可能导致虚拟线程不能卸载,平台线程也被一起阻塞,降低可伸缩性。
- 实战建议:
- 避免在大型同步块/方法内部做阻塞 IO,优先把 IO 放在同步块外;
- 如果有老代码大量用 synchronized 包着 IO,可考虑逐步拆解或改用 ReentrantLock 等更细粒度控制。
问题三:ThreadLocal 还能用吗?
- 虚拟线程是支持 ThreadLocal 的,但因为虚拟线程数量可以非常大,如果在 ThreadLocal 中存放大对象,会导致大量遗留引用、加重 GC 压力。
- 官方后续计划用 ScopedValue / ScopedLocal 这类结构化作用域替代 ThreadLocal 的常见用法(JDK 21 已经提供预览特性)。
- 实战建议:
- 谨慎、少量使用 ThreadLocal,避免缓存容器 / 大 Map;
- 与框架集成时,注意框架是否对虚拟线程下的 ThreadLocal 做过适配(如日志 MDC、事务上下文)。
问题四:和第三方库兼容吗?
- 原则上:虚拟线程对现有同步阻塞代码是“尽量透明兼容”的,大部分 JDBC 驱动、HTTP 客户端等阻塞 IO 库都可直接用。
- 但对以下情况要小心:
- 库内部大量依赖线程池参数/线程数假设(比如用当前线程数来做限流)需要重新评估。
- 使用本地方法、JNI、大量 synchronized 包裹 IO 可能出现“固定”问题,限制性能收益。
- 实战建议:
- 优先选用官方或社区已经声明“支持虚拟线程”的库版本;
- 压测时重点看吞吐、CPU、线程数,以及 JFR 里的 VirtualThreadPinned 事件。
5. 实战落地的几个最佳实践¶
1)IO 密集型服务可以考虑“虚拟线程优先”
- 如 Spring Web、gRPC 客户端、HTTP 调用服务等,可以在业务线程层改成虚拟线程模型(如使用 Executors.newVirtualThreadPerTaskExecutor() 或框架提供的虚拟线程支持)。
- 目标:减少复杂异步/响应式链路,让代码回归简单同步风格,同时把并发能力提升到 1w 级别甚至更高。
2)仍然保留平台线程池用于 CPU 密集型任务
- 对 CPU-heavy 逻辑(规则计算、批量加密等),继续使用固定大小的 ThreadPoolExecutor(平台线程),避免虚拟线程大量排队消耗调度开销。
3)监控与诊断
- 开启 JFR 的虚拟线程相关事件(虚拟线程创建/销毁、Pinned、SubmitFailed 等),帮助诊断“固定”和资源瓶颈问题。
- 监控虚拟线程数量、平台线程数量、请求耗时分布与 CPU 利用率,以判断虚拟线程策略是否真正带来收益。
4)结构化并发 + 虚拟线程(更推荐的高级用法)
- 配合结构化并发(Structured Concurrency)API(JDK 21 提供预览)可以把“一次请求里的多个子任务”放在同一作用域内,统一取消、超时和错误处理,非常适合虚拟线程模型。