Java 21 重磅新特性:虚拟线程(Virtual Threads)实战指南
Java 21 重磅新特性:虚拟线程(Virtual Threads)实战指南
一、引言
2023年9月,Oracle正式发布了 Java 21,这是继 Java 17 之后的又一个长期支持(LTS)版本。在众多新特性中,虚拟线程(Virtual Threads) 无疑是备受关注的焦点。它由 OpenJDK 的 Project Loom 孵化而来,旨在从根本上简化 Java 高并发编程的复杂性,让开发者能够以更低的成本构建高吞吐量的服务器应用。
传统的 Java 线程(平台线程)是操作系统内核线程的轻量级封装,创建成本高、上下文切换开销大,导致在高并发场景下,我们不得不使用线程池等复杂手段来控制线程数量。虚拟线程的出现,彻底改变了这一局面。
二、什么是虚拟线程?
虚拟线程是 java.lang.Thread 的一种新实现,它是一种用户态线程。与传统的平台线程(Platform Thread,即操作系统内核线程)不同,虚拟线程由 JVM 管理,而不是由操作系统管理。
核心概念:
- 平台线程(Platform Thread): 操作系统内核线程的包装,属于 heavyweight 资源。
- 虚拟线程(Virtual Thread): JVM 管理的轻量级线程,属于 lightweight 资源。
- Carrier 线程: 真正执行任务的平台线程。虚拟线程会挂载(Mount)到 Carrier 线程上运行,遇到阻塞操作时会被卸载(Unmount),从而释放 Carrier 线程去执行其他虚拟线程。
三、为什么需要虚拟线程?
在传统 IO 密集型应用中,一个请求通常对应一个线程。当线程进行 IO 操作(如数据库查询、RPC 调用、文件读写)时,线程大部分时间处于阻塞等待状态。由于平台线程是稀缺资源,我们不得不使用线程池来限制并发数,这导致了:
- 线程利用率低: 线程在等待 IO 时被浪费。
- 编程模型复杂: 需要引入异步编程、回调、Reactive 等复杂模型。
- 系统吞吐量受限: 线程数量受限于操作系统资源。
虚拟线程解决了这些问题:百万级虚拟线程可以轻松创建,且阻塞开销极低,让同步编程模型重新成为首选。
四、快速上手:创建虚拟线程
Java 21 提供了多种创建虚拟线程的方式。
4.1 方式一:Thread.startVirtualThread()
public class VirtualThreadDemo {
public static void main(String[] args) {
// 创建并启动一个虚拟线程
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("Hello from Virtual Thread: " + Thread.currentThread());
});
try {
vt.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
4.2 方式二:Thread.ofVirtual()
public class VirtualThreadDemo2 {
public static void main(String[] args) {
// 创建虚拟线程但暂不启动
Thread vt = Thread.ofVirtual()
.name("my-virtual-thread")
.unstarted(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
vt.start();
try {
vt.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
4.3 方式三:Executors.newVirtualThreadPerTaskExecutor()
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadExecutorDemo {
public static void main(String[] args) {
// 每个任务创建一个新的虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running on " + Thread.currentThread());
});
}
}
// try-with-resources 会自动关闭 ExecutorService
}
}
五、实战对比:虚拟线程 vs 平台线程
为了直观感受虚拟线程的强大,我们做一个简单的压力测试:创建大量线程并执行简单的 IO 模拟操作。
5.1 平台线程示例(传统方式)
import java.util.concurrent.CountDownLatch;
public class PlatformThreadTest {
public static void main(String[] args) throws InterruptedException {
int threadCount = 10_000; // 试试 10 万个?可能会 OOM
CountDownLatch latch = new CountDownLatch(threadCount);
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
try {
// 模拟 IO 阻塞
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
thread.start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("Platform Threads: " + threadCount + " threads completed in " + (end - start) + "ms");
}
}
注意: 当
threadCount设置为 10,000 时,平台线程可能勉强运行。如果设置 100,000,大部分机器会直接 OOM 或系统崩溃。
5.2 虚拟线程示例
import java.util.concurrent.CountDownLatch;
public class VirtualThreadTest {
public static void main(String[] args) throws InterruptedException {
int threadCount = 100_000; // 10 万个虚拟线程,轻松应对
CountDownLatch latch = new CountDownLatch(threadCount);
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
Thread vt = Thread.startVirtualThread(() -> {
try {
// 模拟 IO 阻塞
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("Virtual Threads: " + threadCount + " threads completed in " + (end - start) + "ms");
}
}
结果对比:
| 指标 | 平台线程 (10,000) | 虚拟线程 (100,000) | |------|:----------------:|:-----------------:| | 启动时间 | 数秒 | 毫秒级 | | 内存占用 | ~1GB+ | ~几十 MB | | 是否 OOM | 可能 | 不会 | | 代码复杂度 | 需线程池管理 | 简单直接 |
六、虚拟线程的工作原理(简析)
虚拟线程的底层机制可以概括为:挂载(Mount) 和 卸载(Unmount)。
- 当一个虚拟线程开始执行时,JVM 会将其分配给一个 Carrier 线程(平台线程)。
- 当虚拟线程遇到阻塞操作(如
Thread.sleep()、Socket.read()、Future.get())时,JVM 会自动将其从 Carrier 线程上卸载,并保存其执行上下文。 - Carrier 线程被释放,可以去执行其他虚拟线程。
- 当阻塞操作完成时,虚拟线程会重新挂载到某个可用的 Carrier 线程上继续执行。
整个过程对开发者完全透明,我们写的是同步代码,但底层实现了类似异步 IO 的高效调度。
七、使用虚拟线程的注意事项
7.1 避免 synchronized 阻塞
synchronized 是 JVM 级别的锁,虚拟线程在进入 synchronized 块时不会被卸载,会导致 Carrier 线程被阻塞。建议使用 ReentrantLock 替代:
// 不推荐
synchronized (lock) {
// critical section
}
// 推荐
private final Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
7.2 不要池化虚拟线程
虚拟线程是轻量级的,创建和销毁的开销极低,不要将虚拟线程放入线程池中复用。直接用 Executors.newVirtualThreadPerTaskExecutor() 或 Thread.startVirtualThread() 即可。
7.3 ThreadLocal 的合理使用
虚拟线程支持 ThreadLocal,但由于虚拟线程数量可能非常庞大,大量使用 ThreadLocal 会占用大量内存。如果确实需要上下文传递,建议考虑使用 ScopedValue(Java 21 中为孵化特性)。
八、总结
| 特性 | 说明 | |------|------| | ✅ 百万级并发 | 轻松创建数十万甚至百万级虚拟线程 | | ✅ 简化编程模型 | 使用同步代码即可实现高并发 | | ✅ 兼容性极佳 | 现有代码几乎无需修改 | | ✅ 零成本创建 | 创建和销毁开销极低 | | ⚠️ 慎用 synchronized | 会导致 Carrier 线程阻塞 | | ⚠️ 慎用线程池 | 虚拟线程不需要池化 | | ⚠️ 慎用 ThreadLocal | 海量虚拟线程下内存占用大 |
虚拟线程是 Java 并发编程的一次革命性升级。它让我们可以用 同步、顺序的代码 写出 高并发、高吞吐 的应用,彻底告别异步回调地狱。如果你的应用是 IO 密集型(Web 服务、微服务、数据库访问等),那么迁移到虚拟线程将是提升性能和简化代码的最佳选择。
从 Java 21 开始,虚拟线程已经是正式特性(非预览),建议所有 Java 开发者立即开始学习和尝试!
九、参考资料
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)