虚拟线程让“每个任务一个线程”的阻塞式代码承载更高并发,但不会增加 CPU、数据库连接或下游配额。迁移重点是移除平台 线程池造成的线程稀缺,同时保留资源限流、截止时间、取消和可观测性。
Table of contents
运行一千个阻塞任务
下载虚拟线程样例下载, 运行:
java -cp target/classes dev.elaine.concurrency.VirtualThreadSample
程序为一千个任务各创建一个虚拟线程,每个任务阻塞 10 毫秒。输出为:
任务数=1000,校验和=499500
虚拟线程在 Java 21 正式发布。它是 Thread,由 JVM 调度到少量 Carrier 平台线程;阻塞在支持的 JDK I/O 或并发原语时,
通常会卸载,让 Carrier 执行其他虚拟线程。
不要池化虚拟线程
平台线程昂贵,所以线程池限制线程数量;虚拟线程便宜,应为每个任务创建。并发限制应该保护实际稀缺资源:
var databasePermits = new java.util.concurrent.Semaphore(50);
即使能创建十万个虚拟线程,数据库仍可能只有 50 个连接。让剩余任务等待 Semaphore、连接池或有界入口,而不是同时压向 下游。
Pinning 必须区分 JDK 版本
在 Java 21 中,虚拟线程在 synchronized 内执行阻塞操作可能固定 Carrier;Native 或 Foreign Function 调用也可能固定。
JDK 24 的 JEP 491 改进了 monitor 行为,让虚拟线程因 synchronized 阻塞时通常能够卸载。因此“把所有
synchronized 改成 ReentrantLock”不是 Java 25 的通用迁移规则。
Java 25 仍需用 JFR 和线程转储观察 Native 调用、长临界区、Carrier 饱和与下游等待。Oracle 的 JDK 24 重要变化记录了 JEP 491 的行为变化; JEP 444定义了虚拟线程的设计目标和每任务一个线程模型。
观测大量虚拟线程
使用适合虚拟线程的新线程转储:
jcmd <PID> Thread.dump_to_file -format=json threads.json
jcmd <PID> JFR.start name=virtual settings=profile duration=30s filename=virtual.jfr
不要每个虚拟线程都记录高基数指标标签。以路由、任务类型和下游为维度聚合等待时间、并发数、失败和截止时间超限。
适用与不适用场景
虚拟线程适合大量相互独立、以阻塞等待为主的任务。CPU 密集计算仍受核心数限制,应使用受限执行器或 Fork/Join。基于事件 循环且已经稳定的系统不必仅为使用新特性重写。
下一步
阅读并发设计模式与确定性测试,用所有权和可控时序证明并发 不变量。