一、虚拟线程要解决什么问题Java 传统线程平台线程是对操作系统线程的 1:1 封装——每个 Java 线程背后是一个 OS 线程。OS 线程很重一个线程栈默认占 1MB 内存线程切换要陷入内核态开销约 1~3μs。这导致两个硬限制限制维度平台线程虚拟线程数量上限~数千受内存限制200线程≈200MB栈~百万每个约几KB创建开销~1ms系统调用~1μs用户态切换开销1~3μs内核态切换~100ns用户态切换栈内存固定 1MB动态增长按需分配阻塞代价整个 OS 线程阻塞仅虚拟线程挂起载体线程释放虚拟线程的核心思想把线程的调度从 OS 内核搬到 JVM 用户态。虚拟线程运行在少量载体线程ForkJoinPool上遇到阻塞操作时自动让出载体线程让其他虚拟线程继续执行。这样一万个虚拟线程可以跑在几十个载体线程上内存和切换开销都大幅降低。二、核心原理调度模型与内存结构2.1 虚拟线程的调度模型虚拟线程由 JVM 内部的ForkJoinPool调度载体线程数默认等于 CPU 核心数可通过jdk.virtualThreadScheduler.parallelism调整虚拟线程调度模型JDK 21 ​ 载体线程池ForkJoinPool默认 CPU核心数 ┌──────────────────────────────────────────┐ │ Carrier-1 Carrier-2 Carrier-3 Carrier-4 │ │ (运行VT-A) (运行VT-C) (运行VT-E) (运行VT-G) │ └──────────────────────────────────────────┘ ↑ 阻塞时让出 ↑ 唤醒后重新挂载 │ │ ┌─────┴──────────────────────┴─────────────┐ │ 虚拟线程队列百万级 │ │ VT-A(running) VT-B(parked) VT-C(running) │ │ VT-D(parked) VT-E(running) VT-F(parked) │ │ VT-G(running) VT-H(parked) 共百万级 │ └────────────────────────────────────────────┘关键机制当虚拟线程执行阻塞操作如socket.read()、Thread.sleep()时JVM 会自动把它从载体线程上卸载unmount载体线程转去执行下一个就绪的虚拟线程。阻塞操作完成后虚拟线程被重新调度到某个载体线程上继续执行remount。// JDK 21创建和运行虚拟线程的几种方式 public class VirtualThreadDemo { ​ // 方式一直接创建虚拟线程并启动 public void startDirect() { Thread vt Thread.ofVirtual().start(() - { System.out.println(虚拟线程运行中 Thread.currentThread()); }); } ​ // 方式二通过虚拟线程 Per-Task Executor 提交任务 public void startViaExecutor() throws Exception { // 每个任务一个虚拟线程Executors.newVirtualThreadPerTaskExecutor try (var executor Executors.newVirtualThreadPerTaskExecutor()) { // 提交 10000 个任务每个跑在一个虚拟线程上 var futures new java.util.ArrayListjava.util.concurrent.FutureInteger(); for (int i 0; i 10000; i) { final int taskId i; futures.add(executor.submit(() - { Thread.sleep(100); // 模拟 IO 阻塞 return taskId; })); } // 等待全部完成 int sum 0; for (var f : futures) sum f.get(); System.out.println(完成 10000 个任务sum sum); } } ​ // 方式三判断当前是否为虚拟线程 public void checkThreadType() { boolean isVirtual Thread.currentThread().isVirtual(); System.out.println(当前线程是虚拟线程吗 isVirtual); } } 原理要点虚拟线程的Thread对象和平台线程一样有完整的线程 APIgetName、getStackTrace等但底层不绑定 OS 线程。isVirtual()是 JDK 21 新增的判断方法。2.2 栈结构为什么虚拟线程内存这么小平台线程的栈在创建时就分配固定大小默认 1MB无论用没用到都占着。虚拟线程的栈是动态的——初始只有几百字节的栈帧在堆上随着方法调用深度增长按需扩展方法返回后收缩。栈维度平台线程虚拟线程存储位置OS 线程栈连续内存Java 堆对象形式初始大小1MB固定~数百字节最大深度受栈大小限制受堆内存限制增长方式不可增长按需扩展copy 栈帧阻塞时栈内存仍占用栈帧快照存堆上载体线程释放⚠️ 注意虚拟线程的栈存在堆上意味着大量虚拟线程会增加 GC 压力。但每个虚拟线程的栈通常只有几 KB远小于平台线程的 1MB百万虚拟线程也只占几 GB 堆。三、载体线程与Continuation阻塞挂起的底层虚拟线程能阻塞不占线程的秘密在于Continuation——JDK 内部对执行栈的快照与恢复机制。// JDK 21 内部机制简化展示原理Continuation 是内部 API 不直接暴露 // 当虚拟线程执行到阻塞操作如 Socket.read时 ​ // 1. JVM 检测到阻塞操作通过 Unsafe.park 或 jdk.internal.vm.Continuation // 2. 将当前虚拟线程的执行栈快照保存为 Continuation 对象存到堆上 // 3. 从载体线程卸载unmount载体线程回到 ForkJoinPool 执行下一个任务 // 4. 阻塞操作完成后如数据到达Continuation 被提交回调度器 // 5. 调度器将 Continuation 挂载到某个载体线程remount恢复执行 ​ // 这个过程对开发者透明代码写法和平台线程完全一样 Thread vt Thread.ofVirtual().start(() - { // 下面这行看似阻塞了线程实际只挂起了虚拟线程载体线程是自由的 var data socket.read(buffer); // 阻塞 IO自动 unmount/remount process(data); });关键JDK 21 对java.util.concurrent的阻塞操作LockSupport.park、BlockingQueue.take、Thread.sleep、Socket I/O都做了适配能自动触发虚拟线程的 unmount/remount。这就是虚拟线程用同步代码风格获得异步性能的核心。四、实测环境准备项目配置操作系统Windows 11 22H2CPUAMD Ryzen 7 5800H 8核 3.2GHz内存32GB DDR4 3200MHzJDKOpenJDK 21.0.2 (Temurin)JVM 堆-Xms4g -Xmx4g压测工具JMHJava Microbenchmark Harness压测代码模拟 IO 密集型 Web 服务的场景package com.example.vtbench; ​ import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.*; import java.util.ArrayList; import java.util.List; ​ // 对比平台线程池 vs 虚拟线程池在 IO 密集型任务下的吞吐量 State(Scope.Benchmark) BenchmarkMode(Mode.Throughput) OutputTimeUnit(TimeUnit.SECONDS) Warmup(iterations 3, time 5) Measurement(iterations 5, time 10) Fork(1) public class VirtualThreadBenchmark { ​ // 模拟 IO 密集型任务sleep 10ms 模拟网络/数据库等待 private static final int TASK_COUNT 10000; // 每轮提交 10000 个任务 private static final long IO_DELAY_MS 10; // 每个 IO 操作 10ms ​ Benchmark Threads(1) public void platformThreadPool(Blackhole bh) throws Exception { // 平台线程池固定 200 线程典型 Web 服务器配置 ExecutorService pool Executors.newFixedThreadPool(200); try { ListFutureInteger futures new ArrayList(); for (int i 0; i TASK_COUNT; i) { futures.add(pool.submit(() - { Thread.sleep(IO_DELAY_MS); // 模拟 IO 阻塞 return 1; })); } for (FutureInteger f : futures) { bh.consume(f.get()); // 等待全部完成 } } finally { pool.shutdown(); } } ​ Benchmark Threads(1) public void virtualThreadPool(Blackhole bh) throws Exception { // 虚拟线程池每个任务一个虚拟线程不限数量 ExecutorService pool Executors.newVirtualThreadPerTaskExecutor(); try { ListFutureInteger futures new ArrayList(); for (int i 0; i TASK_COUNT; i) { futures.add(pool.submit(() - { Thread.sleep(IO_DELAY_MS); // 模拟 IO 阻塞 return 1; })); } for (FutureInteger f : futures) { bh.consume(f.get()); // 等待全部完成 } } finally { pool.shutdown(); } } }五、实测一IO 密集型任务吞吐量对比# JDK 21 运行 JMH 压测 java -Xms4g -Xmx4g -jar vtbench.jar实测结果10000 个 IO 任务每个 sleep 10ms指标平台线程池200线程虚拟线程池差异总耗时512ms106ms↓79%吞吐量任务/秒1953194339383%峰值线程数200~8载体线程载体线程极少峰值内存栈~200MB~40MB↓80%任务平均等待时间50ms1ms↓98% 实测环境Windows 11 / AMD 5800H 8核 / JDK 21.0.2 / 10000 任务每个 IO 10ms。虚拟线程吞吐量是平台线程池的 4.8 倍因为 10000 个虚拟线程可以几乎同时 sleep而 200 个平台线程要分 50 批处理。为什么差这么多平台线程池只有 200 个线程10000 个任务要分 50 批10000/200每批 10ms总耗时 500ms。虚拟线程 10000 个同时跑每个独立 sleep 10ms总耗时约 10ms调度开销≈106ms。这是阻塞不占线程带来的直接收益。六、实测二CPU 密集型任务对比虚拟线程的优势在 IO 密集型场景。CPU 密集型任务不涉及阻塞虚拟线程没有优势Benchmark Threads(1) public void cpuIntensivePlatform(Blackhole bh) throws Exception { ExecutorService pool Executors.newFixedThreadPool(8); // CPU 核心数 try { ListFutureLong futures new ArrayList(); for (int i 0; i 100; i) { final int n i; futures.add(pool.submit(() - { // 纯 CPU 计算斐波那契 long result fib(30); return result n; })); } for (FutureLong f : futures) bh.consume(f.get()); } finally { pool.shutdown(); } } ​ // 同样的 CPU 密集型任务用虚拟线程池 Benchmark Threads(1) public void cpuIntensiveVirtual(Blackhole bh) throws Exception { ExecutorService pool Executors.newVirtualThreadPerTaskExecutor(); try { ListFutureLong futures new ArrayList(); for (int i 0; i 100; i) { final int n i; futures.add(pool.submit(() - { long result fib(30); return result n; })); } for (FutureLong f : futures) bh.consume(f.get()); } finally { pool.shutdown(); } } ​ // 斐波那契递归CPU 密集型 private long fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }CPU 密集型实测结果指标平台线程池8线程虚拟线程池差异吞吐量任务/秒14.214.1基本持平峰值线程数8~8相同峰值 CPU 利用率98%98%相同 结论CPU 密集型任务虚拟线程没有优势。因为 CPU 计算不阻塞虚拟线程不会让出载体线程100 个虚拟线程还是跑在 8 个载体线程上和直接用 8 线程池效果相同。虚拟线程的价值在 IO 阻塞场景。七、踩坑记录❌ 错误做法一在虚拟线程中使用 synchronized// 错误虚拟线程中使用 synchronized 会钉住载体线程JDK 21 已知问题 public synchronized String badMethod() { // synchronized 持有监视器锁时虚拟线程无法 unmount // 载体线程被钉住pin阻塞期间不能执行其他虚拟线程 return db.query(); // 这里的 IO 阻塞会钉住整个载体线程 }问题JDK 21 中synchronized块会导致虚拟线程钉住载体线程Thread Pinning阻塞期间载体线程无法释放。100 个虚拟线程进入同一个 synchronized 方法时需要 100 个载体线程失去虚拟线程的优势。✅ 正确做法// 正确用 ReentrantLock 替代 synchronizedJDK 21 虚拟线程友好 private final ReentrantLock lock new ReentrantLock(); ​ public String goodMethod() { lock.lock(); try { // ReentrantLock 的阻塞会正确触发虚拟线程 unmount return db.query(); // IO 阻塞时虚拟线程正常让出载体线程 } finally { lock.unlock(); } }原因ReentrantLock基于 AQS使用LockSupport.park阻塞JDK 21 已适配虚拟线程的 unmount/remount。synchronized基于 JVM 内置监视器JDK 21 尚未完全适配计划在后续版本修复。在虚拟线程场景下全部用ReentrantLock替代synchronized。❌ 错误做法二用虚拟线程跑 CPU 密集型长任务// 错误用虚拟线程跑长时间 CPU 计算载体线程被占满 Thread.ofVirtual().start(() - { // 纯 CPU 计算 30 秒不阻塞不释放载体线程 long result heavyCompute(30); // 载体线程被独占 30 秒 });问题虚拟线程不阻塞时不让出载体线程。8 个载体线程跑 8 个 CPU 密集虚拟线程后其他虚拟线程全部排队等待。✅ 正确做法// 正确CPU 密集型用固定大小平台线程池等于 CPU 核心数 ExecutorService cpuPool Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); cpuPool.submit(() - heavyCompute(30)); ​ // IO 密集型用虚拟线程 Thread.ofVirtual().start(() - { var data httpClient.get(url); // IO 阻塞虚拟线程优势 process(data); });原因CPU 密集型任务用平台线程池线程数 CPU 核心数避免过度竞争IO 密集型任务用虚拟线程充分利用阻塞等待时间。两者混用是最佳实践。八、常见问题Q: 虚拟线程能完全替代平台线程吗A: 不能。虚拟线程适合 IO 密集型、高并发、阻塞多的场景Web 服务器、微服务调用。平台线程适合 CPU 密集型、需要 OS 线程特性的场景JNI 调用、需要线程优先级、需要Thread.getAllStackTraces。两者可以混用——CPU 密集用平台线程池IO 密集用虚拟线程。Q: 虚拟线程和 Reactor/响应式编程WebFlux有什么区别A: 虚拟线程用同步代码风格实现异步效果——代码写法和普通线程一样JVM 底层自动处理 unmount/remount。响应式编程需要用 Mono/Flux 链式调用代码可读性差、调试困难。虚拟线程的目标就是让开发者不再需要响应式编程——同步代码 虚拟线程 异步性能。Spring Boot 3.2 已支持虚拟线程spring.threads.virtual.enabledtrue即可让 Tomcat 用虚拟线程处理请求。Q: 虚拟线程的 ThreadLocal 有什么坑A: 虚拟线程数量可能百万级每个都有独立的 ThreadLocal 副本内存可能爆炸。JDK 21 引入了ScopedValue预览特性替代 ThreadLocal——它是不可变的、作用域限定的虚拟线程结束后自动清理。如果必须用 ThreadLocal注意控制虚拟线程数量或用InheritableThreadLocal谨慎传递。总结虚拟线程的核心是用户态调度百万虚拟线程跑在少量载体线程上阻塞时自动 unmount 释放载体线程阻塞完成后 remount 继续执行。栈存在堆上按需增长每个虚拟线程只占几 KB。IO 密集型场景性能提升显著实测 10000 个 IO 任务虚拟线程吞吐量比 200 线程池高 383%内存降低 80%。CPU 密集型场景无优势不阻塞不让出载体线程。synchronized 会钉住载体线程JDK 21 中 synchronized 块导致虚拟线程无法 unmount需用 ReentrantLock 替代。这是当前版本最大的迁移成本。混用是最佳实践CPU 密集型用平台线程池线程数 CPU 核心数IO 密集型用虚拟线程两者配合发挥各自优势。 记住虚拟线程不是银弹它解决的是IO 阻塞浪费线程的问题。如果你的服务是 CPU 密集型虚拟线程帮不了你如果是 IO 密集型的高并发服务虚拟线程能让你的同步代码达到响应式编程的性能。你可能还想问Q: Spring Boot 怎么启用虚拟线程A: Spring Boot 3.2 加配置spring.threads.virtual.enabledtrueTomcat 会用虚拟线程处理每个 HTTP 请求。注意同步调用的下游服务数据库、HTTP 客户端的连接池也要相应调大否则虚拟线程会在连接池上阻塞。Q: 虚拟线程的调试和线程dump怎么看A:jstack和jcmd Thread.dump都支持虚拟线程会标注 virtual。JDK 21 的Thread.ofVirtual().start()创建的线程在 IDE 调试器中也能正常断点。注意百万虚拟线程的 thread dump 会非常大建议用jcmd pid Thread.dump_to_file -formatjson输出结构化格式。你在实际项目中用过虚拟线程吗遇到什么坑欢迎评论区交流