从底层原理到生产落地,一张图彻底搞懂
💡 一句话本质
虚拟线程是JVM 实现的轻量级用户态线程, 采用 M:N 调度模型:M 个虚拟线程映射到 N 个操作系统线程。 它不是"更快的线程",而是"更多的线程"——百万级并发也只占用少量系统资源, 专门解决 IO 密集型场景 的线程数量瓶颈。
🏭 平台线程:1:1 模型
特点:
🚀 虚拟线程:M:N 模型
特点:
🏢 通俗比喻
操作系统线程 = 工位(数量有限,租一个很贵); 虚拟线程 = 打工人(可以很多,人轻量)。 打工人工作时占用工位,去接水/上厕所(IO阻塞)时就离开工位,让别人用。 调度器就是行政,负责安排谁什么时候用哪个工位。
Carrier Thread
ForkJoinPool
Segmented Stack
📊 栈内存结构对比
平台线程:固定连续栈
虚拟线程:分段栈
① 创建虚拟线程
Thread.ofVirtual().start(...) 创建虚拟线程,
分配初始栈块,任务进入调度队列,此时未挂载。
② 挂载(Mount)执行
调度器选中一个空闲载体线程,把虚拟线程的栈帧绑定到载体线程上, 开始执行代码。此时虚拟线程 = 跑在载体线程上。
③ 阻塞时卸载(Unmount)
遇到 IO、sleep、LockSupport.park() 等阻塞操作时, 虚拟线程会从载体线程上卸载下来, 栈数据完整保存到堆内存,载体线程被释放去跑其他虚拟线程。
④ 唤醒后重新挂载
IO 完成、sleep 结束等唤醒事件发生后, 虚拟线程重新进入调度队列,等待分配空闲载体线程, 栈数据重新挂载,从上次中断的地方继续执行。
⑤ 执行结束,资源释放
任务执行完毕,虚拟线程退出,分段栈内存全部释放, 载体线程继续待命,执行下一个虚拟线程。
成本:微秒级,涉及内核态切换、缓存失效
成本:纳秒级,相当于一次方法调用
🎯 核心结论
虚拟线程的高效,本质是把昂贵的内核态线程切换, 换成了廉价的用户态栈切换。 对于大量等待 IO 的场景,线程大部分时间都在阻塞, 用少量载体线程就能带动海量虚拟线程,吞吐量直接起飞。
误区1:虚拟线程比平台线程快
错。单个任务执行速度没有区别,甚至因为挂载卸载还有轻微开销。 它的优势是数量多、并发高,IO密集场景下整体吞吐量提升,而不是单个任务变快。
误区2:CPU 密集型用虚拟线程也能提速
错。CPU 满负载时,载体线程本身就没空,虚拟线程再多也没用。 虚拟线程只适合IO 密集型场景(网络请求、数据库、文件读写)。
误区3:虚拟线程要用线程池管理
错。虚拟线程创建成本几乎为0,每个任务新建一个就行,不要池化。 池化是平台线程的产物,因为创建成本高才需要池。
误区4:synchronized 会阻塞载体线程
对(这是一个坑)。synchronized 目前会导致载体线程一起阻塞,
相当于退化成平台线程。高并发场景建议用 ReentrantLock 替代。
推荐:每个任务新建虚拟线程 + 结构化并发
// ✅ 推荐:使用 Executors.newVirtualThreadPerTaskExecutor()
// 每个任务一个虚拟线程,自动管理生命周期
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List> futures = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
futures.add(executor.submit(() -> callRemoteApi()));
}
// 等待所有结果
for (Future f : futures) {
f.get();
}
} // 退出自动清理,不会泄漏
// ✅ 更推荐:结构化并发(Java 21 预览)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> userService.query(id));
var order = scope.fork(() -> orderService.query(id));
scope.join().throwIfFailed();
return new Result(user.get(), order.get());
}
✅ 正确做法
❌ 错误做法
| 参数 | 默认值 | 说明 | 调优建议 |
|---|---|---|---|
-XX:ParallelGCThreads |
CPU核心数 | 同时也是虚拟线程载体线程池的大小 | IO密集型可适当调大,如 2*CPU |
-XX:MaxDirectMemorySize |
等于堆大小 | NIO 直接内存大小 | 高并发IO场景适当调大,避免OOM |
-XX:VirtualThreadStackSize |
0(自适应) | 虚拟线程初始栈大小 | 一般保持默认,深度递归可适当调大 |
-XX:+UseVThreadSchedulerPinning |
true | 是否允许虚拟线程固定在载体上 | 保持默认,减少不必要的挂载卸载 |
-XX:ReservedCodeCacheSize |
240M | JIT 代码缓存 | 大应用建议调到 512M,避免编译降级 |
-XX:+UseZGC |
关闭 | 启用 ZGC 垃圾回收器 | 大堆+虚拟线程强烈推荐,低延迟 |
-XX:+ZGenerational |
关闭 | 启用分代 ZGC | Java 21+ 推荐开启,提升吞吐量 |
8 核 16G 机器 IO 密集型服务推荐配置
java -Xms8g -Xmx8g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:ParallelGCThreads=16 \
-XX:MaxDirectMemorySize=2g \
-XX:ReservedCodeCacheSize=512m \
-jar your-app.jar
开启虚拟线程日志
-Djdk.virtualThreadScheduler.parallelism=16
-Djdk.virtualThreadScheduler.maxPoolSize=256
-XX:+TraceVirtualThreads
常用诊断命令
# 查看虚拟线程数量
jcmd Thread.print | grep -c Virtual
# 查看载体线程池状态
jcmd VM.info
# 持续观察线程状态
watch -n 1 'jstack | grep state'
⚠️ 调优注意事项
底层原理
M:N 调度模型 + 载体线程 + 分段栈 + 用户态切换
核心价值
IO密集场景下,极低内存成本获得极高并发量
最佳实践
每个任务新建,配合结构化并发,用ReentrantLock
调优方向
载体线程数 + ZGC + 直接内存 + 代码缓存
Java 21 虚拟线程 · 原理+实践+调优 漫画图解 | 从入门到生产落地