Java 21 虚拟线程深度解析
Java 21 虚拟线程深度解析
一、产生背景
传统平台线程的局限性
Java 的传统线程(Platform Thread)是对操作系统线程的 1:1 封装。每创建一个 Thread 对象,JVM 就向 OS 申请一个内核线程。这带来了三个硬性瓶颈:
① 内存开销大
每个平台线程默认占用约 1MB 的栈空间(可通过 -Xss 调整,但下限约 256KB)。1 万个并发请求 = 约 10GB 内存仅用于线程栈,这在高并发系统中几乎不可接受。
② 上下文切换成本高
OS 调度内核线程时,需要保存/恢复 CPU 寄存器、程序计数器、栈指针等完整状态,一次切换耗时约 1~10 微秒,且会污染 CPU 缓存。线程数越多,调度开销越大。
③ 吞吐量天花板明显
在典型的 Web 服务中,线程大部分时间在等待 IO(数据库查询、HTTP 调用)——等待期间线程被阻塞,但 OS 线程资源并未释放。线程池大小(通常几百个)直接决定了服务的最大并发吞吐量上限。
传统模型:
请求 1 → 线程A [处理 10ms] [等待DB 50ms] [处理 5ms] → 响应
←————线程A 被占用整整 65ms ————————————————→
↑ 等待期间完全浪费
Project Loom 的立项动机
Project Loom 于 2017 年 由 Ron Pressler 主导启动,核心目标只有一个:让简单的同步代码获得异步代码的性能。
经历了漫长的孵化期后,虚拟线程在 Java 19(JEP 425,预览)→ Java 20(JEP 436,二次预览)→ Java 21(JEP 444,正式发布) 完成了从实验到生产就绪的完整旅程。
与 Go / Node.js 的路线对比
| 特性 | Java 虚拟线程 | Go Goroutine | Node.js 事件循环 |
|---|---|---|---|
| 编程模型 | 同步阻塞(不改变代码风格) | 同步阻塞 | 异步回调 / async-await |
| 调度方式 | JVM 用户态调度 | Go runtime 调度 | 单线程事件循环 |
| 栈大小 | 动态增长,初始 KB 级 | 动态增长,初始 ~8KB | 无独立栈概念 |
| 与已有代码兼容性 | ✅ 极高,Thread API 不变 | ❌ 需重写 | ❌ 需改异步风格 |
| CPU 密集型 | 无优势 | 自动多核并行 | ❌ 单线程限制 |
Java 的选择是兼容性优先:不引入新的关键字(无 async/await),不强迫开发者改变心智模型,让几十亿行现有 Java 代码无缝受益。
二、技术原理
核心类比:虚拟线程 vs 平台线程
把 OS 内核线程 想象成城市里的出租车(数量有限,昂贵);把虚拟线程想象成共享单车(数量近乎无限,轻量)。出租车(Carrier Thread)负责真正在路上跑,共享单车(虚拟线程)需要出行时"挂载"到出租车上,不需要时"卸载"归还,让出租车去载下一个用户。
底层三要素
① Carrier Thread(承载线程)
即真实的平台线程,数量默认等于 CPU 核心数(通过 jdk.virtualThreadScheduler.parallelism 可调)。它是虚拟线程实际执行的"宿主"。
② Continuation(续体)
这是 Loom 最核心的底层抽象。Continuation 封装了虚拟线程的执行上下文(栈帧、局部变量、程序计数器),支持在任意阻塞点暂停(yield)并在稍后恢复。本质上是 JVM 对"可挂起计算"的原生支持。
③ 调度器(Scheduler)
虚拟线程默认使用一个专用的 ForkJoinPool(工作窃取模式)作为调度器。它负责将就绪的虚拟线程分配给空闲的 Carrier Thread 执行。
挂载与卸载机制
虚拟线程生命周期:
[就绪] --调度器分配--> [挂载到 Carrier Thread] --执行代码--> [遇到阻塞IO]
|
[卸载,Continuation保存状态]
[Carrier Thread 去执行其他虚拟线程]
|
[IO完成,重新入队]
|
[再次挂载到(可能不同的)Carrier Thread]
[从断点处继续执行]
阻塞时,JVM 底层(java.io、java.net、java.nio 等)会触发 Continuation.yield(),将当前虚拟线程从 Carrier Thread 上"剥离",Carrier Thread 立即可用于运行其他任务。IO 完成后,虚拟线程重新进入调度队列等待恢复。全程无 OS 线程阻塞,无上下文切换损耗。
代码示例:虚拟线程创建方式
import java.util.concurrent.Executors;
public class VirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
// ① 方式一:Thread.ofVirtual() 创建单个虚拟线程
Thread vt = Thread.ofVirtual()
.name("my-vthread-1")
.start(() -> {
System.out.println("运行在: " + Thread.currentThread());
// 输出类似: VirtualThread[#21,my-vthread-1]/runnable@ForkJoinPool...
});
vt.join();
// ② 方式二:虚拟线程工厂(适合与旧代码集成)
var factory = Thread.ofVirtual().name("worker-", 0).factory();
Thread t2 = factory.newThread(() -> System.out.println("工厂创建: " + Thread.currentThread()));
t2.start();
t2.join();
// ③ 方式三:专用 ExecutorService(最推荐的生产用法)
// 每个任务提交时自动创建一个新虚拟线程,无需手动管理
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
final int id = i;
executor.submit(() -> {
// 模拟 IO 阻塞(如数据库查询)
Thread.sleep(100);
System.out.println("任务 " + id + " 完成");
return id;
});
}
} // try-with-resources 自动 shutdown + awaitTermination
// ④ 判断当前线程是否是虚拟线程
Thread.ofVirtual().start(() -> {
System.out.println("是虚拟线程: " + Thread.currentThread().isVirtual()); // true
}).join();
}
}
三、应用场景
最适合:IO 密集型场景
虚拟线程的收益与线程阻塞时间占比正相关。典型受益场景:
- 数据库查询:JDBC 阻塞调用,等待时间占 80%+
- 微服务间 HTTP 调用:
HttpClient、RestTemplate、OpenFeign - 文件读写:大量文件处理任务
- 消息队列消费:Kafka/RabbitMQ 的长轮询消费者
不适合或收益有限的场景
- CPU 密集型计算:图像处理、加密运算、机器学习推理。虚拟线程无法让 1 个 CPU 核心变成 2 个,该并行还是要用
ForkJoinPool或parallelStream。 - 已经是响应式架构的系统:使用 Spring WebFlux + Reactor 的系统已经达到了高吞吐,迁移收益有限且可能引入复杂性。
在 Spring Boot 3.x 中启用虚拟线程
Spring Boot 3.2+ 提供了开箱即用的支持,一行配置即可:
# application.yml
spring:
threads:
virtual:
enabled: true # Tomcat、Jetty、调度线程池全部切换为虚拟线程
或通过代码配置(Spring Boot 3.1 兼容写法):
@Configuration
public class VirtualThreadConfig {
// 替换 Tomcat 的线程池为虚拟线程
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
// 替换 Spring @Async 的线程池
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
}
实战示例:并发请求多个外部 API
// 场景:聚合 3 个下游服务的数据,总耗时取决于最慢的那个
// ——— 传统线程池写法 ———
ExecutorService pool = Executors.newFixedThreadPool(3); // 线程是稀缺资源,要限制
Future<String> f1 = pool.submit(() -> callServiceA()); // 各自阻塞线程池线程
Future<String> f2 = pool.submit(() -> callServiceB());
Future<String> f3 = pool.submit(() -> callServiceC());
String result = f1.get() + f2.get() + f3.get(); // 等待全部完成
// ——— 虚拟线程写法(Java 21)———
// 无需限制线程数,虚拟线程开销极小
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> v1 = executor.submit(() -> callServiceA());
Future<String> v2 = executor.submit(() -> callServiceB());
Future<String> v3 = executor.submit(() -> callServiceC());
String result2 = v1.get() + v2.get() + v3.get();
// 三个服务各耗时 200ms,总耗时约 200ms(并行),
// 且不占用任何 OS 线程等待——Carrier Thread 全程在处理其他任务
}
四、最佳实践
① 不要池化虚拟线程
// ❌ 错误:对虚拟线程使用固定大小线程池
ExecutorService wrong = Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());
// 限制了并发数,且失去了虚拟线程"按需创建"的优势
// ✅ 正确:每任务一个虚拟线程,让 JVM 调度器自己控制
ExecutorService correct = Executors.newVirtualThreadPerTaskExecutor();
虚拟线程的设计哲学是**“廉价且短暂”**,创建成本极低(微秒级),池化反而会引入不必要的等待和复杂性。
② Pinned Thread 问题——synchronized 的陷阱
这是虚拟线程最重要的注意事项。当虚拟线程在 synchronized 块内执行阻塞操作时,它无法被卸载,会将 Carrier Thread 一起钉住(Pin),退化为平台线程行为。
// ❌ 危险:synchronized 内部发生阻塞
synchronized (lock) {
String result = httpClient.send(request, ...); // 阻塞!Carrier Thread 被 Pin 住
}
// ✅ 正确:改用 ReentrantLock(支持虚拟线程卸载)
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
String result = httpClient.send(request, ...); // 可以正常卸载
} finally {
lock.unlock();
}
排查命令:
# 运行时检测 Pin 事件(添加 JVM 参数)
-Djdk.tracePinnedThreads=full # 打印详细堆栈
-Djdk.tracePinnedThreads=short # 仅打印 Pin 发生的帧
# 或使用 JFR 事件:jdk.VirtualThreadPinned
③ ThreadLocal 慎用 → 推荐 Scoped Values
// ⚠️ 问题:虚拟线程数量可达百万级,ThreadLocal 数据随线程生命周期持有,
// 若线程"复用"或者数据量大,内存压力剧增
// 传统写法
static ThreadLocal<User> currentUser = new ThreadLocal<>();
currentUser.set(user); // 设置
currentUser.get(); // 获取
currentUser.remove(); // 必须手动清理!否则内存泄漏
// ✅ Java 21 推荐:ScopedValue(JEP 446,预览)
// 不可变、自动作用域管理、对虚拟线程友好
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
// 在此 lambda 范围内,CURRENT_USER.get() 返回 user
processRequest();
}); // 作用域结束自动清理,无泄漏风险
④ 结构化并发(Structured Concurrency)
结构化并发将多个并发任务的生命周期限定在一个词法作用域内,避免任务泄漏:
import java.util.concurrent.StructuredTaskScope;
// 场景:并发查询用户信息和订单,任一失败则全部取消
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 分叉:提交子任务(运行在虚拟线程上)
StructuredTaskScope.Subtask<User> userTask = scope.fork(() -> fetchUser(userId));
StructuredTaskScope.Subtask<List<Order>> orderTask = scope.fork(() -> fetchOrders(userId));
// 汇合:等待全部完成,若有异常则传播
scope.join().throwIfFailed();
// 安全使用结果(能到这里说明两个任务都成功了)
return new UserProfile(userTask.get(), orderTask.get());
} // 作用域结束,所有子任务必然已完成或被取消——无泄漏
五、未来演进方向
Scoped Values 正式化(JEP 446 → 487)
ScopedValue 作为 ThreadLocal 的现代替代方案,在 Java 21/22 处于预览状态,预计在 Java 23/24 正式转正。其核心优势:不可变(天然线程安全)、层次化继承(父线程值对子虚拟线程可见)、自动作用域管理。
结构化并发成熟(JEP 453)
结构化并发同样处于预览阶段,未来将提供更丰富的 ShutdownOnSuccess(竞速模式,取第一个成功结果)等策略,并与 ScopedValue 深度集成,形成完整的"现代 Java 并发编程三件套":虚拟线程 + 结构化并发 + Scoped Values。
对响应式编程的冲击与共存
| 维度 | 虚拟线程 | Reactive (WebFlux) |
|---|---|---|
| 代码可读性 | ✅ 直观,同步风格 | ❌ 链式操作,学习曲线陡 |
| 调试友好性 | ✅ 普通线程堆栈 | ❌ 堆栈碎片化 |
| 超高并发(百万级) | ✅ 适合 IO 密集 | ✅ 适合极致吞吐 |
| 背压控制 | ❌ 无原生支持 | ✅ Reactor 内置 |
| 成熟度 | 新,生态适配中 | 成熟,但复杂 |
Spring 官方的态度是:新项目优先考虑虚拟线程;已有 WebFlux 项目继续使用响应式;两者可以共存于同一应用中。
Java 并发模型的整体走向
Java 并发的未来清晰可见:从"管理线程资源"转向"描述任务结构"。开发者将越来越少关心线程池大小、线程生命周期,而是专注于业务逻辑的并发结构——哪些任务并行、哪些串行、失败如何传播——这些由 JVM 和框架统一处理。
思考题(自测)
- 虚拟线程在
synchronized块内执行Thread.sleep()会发生 Pin 吗?为什么?(提示:sleep 不涉及 IO,思考 JVM 对 sleep 的实现) - 将一个使用大量
ThreadLocal存储上下文的老系统迁移到虚拟线程,可能会引发什么问题?如何评估迁移风险? - 为什么虚拟线程的调度器选择 ForkJoinPool 而不是普通的
ThreadPoolExecutor?工作窃取(work-stealing)在这里有何优势?
学习路线图(前置知识补充)
虚拟线程
├── 必须掌握
│ ├── Java 并发基础(synchronized、volatile、happens-before)
│ ├── JUC 核心(ReentrantLock、CompletableFuture、ExecutorService)
│ └── IO 模型(BIO/NIO/AIO 区别,Linux epoll 原理)
│
├── 深入理解推荐
│ ├── JVM 栈帧结构与方法调用机制
│ ├── ForkJoinPool 工作窃取算法
│ └── Continuation 理论(可研究 Kotlin Coroutines 对比理解)
│
└── 生产落地必读
├── Spring Boot 3.x 虚拟线程集成文档
├── JEP 444 / 453 / 446 原文
└── 你所用 JDBC 驱动 / 连接池是否已适配虚拟线程
(HikariCP 已适配;某些驱动内部仍用 synchronized)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)