关键在于让载体线程不空转、不阻塞、不浪费:虚拟线程通过JVM细粒度切分CPU与I/O时间片实现百万吞吐,需用newVirtualThreadPerTaskExecutor执行器、在可挂起操作中触发卸载、配合ZGC及JFR监控调度健康度。
直接用虚拟线程压榨单机百万吞吐,关键不在“堆数量”,而在让载体线程不空转、不阻塞、不浪费。jdk 21 的 virtualthread 不是提速单请求,而是把 cpu 和 i/o 时间片切得更细、复用得更满。
选对执行器:用 newVirtualThreadPerTaskExecutor 而非手动 new Thread
虚拟线程的调度优势依赖 JVM 内置的结构化并发机制。手动调用 Thread.ofVirtual().start() 虽然可行,但缺乏生命周期统一管理,容易泄漏或难以等待完成。
推荐写法:
- 使用
try (var executor = Executors.newVirtualThreadPerTaskExecutor())—— 自动 close,自动 awaitTermination - 避免复用同一个 executor 处理长周期混合任务(比如一部分极快、一部分超慢),否则可能拖慢整体调度节奏
- 不要把它当成传统线程池来“调优核心数”,它没有“核心线程”概念;它的吞吐取决于 I/O 密度和载体线程利用率
让阻塞真正“卸载”:只在可挂起操作上阻塞
虚拟线程的魔法发生在阻塞点:当它调用 Thread.sleep()、Object.wait()、NIO Channel 操作、或支持 Loom 的数据库驱动(如 PostgreSQL 42.7+)时,JVM 才会将其从载体线程上卸载,腾出资源跑别的任务。
注意这些常见陷阱:
-
synchronized块内做耗时操作(如 DB update)会阻塞整个载体线程——改用ReentrantLock.lockInterruptibly()或异步驱动 - 老版本 JDBC 驱动(如 MySQL Connector/J 8.0.32 之前)仍是阻塞式,会卡住载体线程——必须升级到支持 virtual thread 的驱动
- 自定义的
while(!ready) Thread.yield()这类忙等逻辑,无法触发卸载,纯耗 CPU
内存与 GC:别让百万线程变成 GC 地狱
每个虚拟线程初始栈仅几百字节,存在堆中,由 GC 管理。但百万级并发意味着百万个对象持续分配、挂起、恢复、销毁。
实操建议:
- JVM 参数加
-XX:+UseZGC或-XX:+UseShenandoahGC,降低 GC 停顿对调度的影响 - 避免在虚拟线程任务中创建大临时对象(如 megabyte 级 byte[]),否则堆压力陡增
- 用 JFR(JDK Flight Recorder)开启
jdk.VirtualThreadMount和jdk.VirtualThreadUnmount事件,观察挂载/卸载频率是否健康
压测不是堆任务,而是看调度饱和度
百万吞吐 ≠ 启动一百万个 executor.submit(() -> { Thread.sleep(10); })。真实瓶颈常在下游:网卡打满、连接池耗尽、DB 连接不够、文件句柄不足。
有效压测姿势:
- 先用
Thread.sleep(1)模拟轻量 I/O,确认单机能否稳住 50 万 RPS;再逐步接入真实 DB/HTTP 客户端 - 监控指标重点看:carrier thread CPU 使用率(应接近 90%+)、virtual thread mount/unmount ratio(理想 > 5:1)、GC pause time(单次 < 10ms)
- 用
jcmd <pid> VM.native_memory summary查看 carrier thread 实际 OS 线程数(通常等于 CPU 核数),确认没意外膨胀

















