06 虚拟线程深入解读
虚拟线程(Virtual Thread)是 Java 21 引入的重磅特性,它让 Java 具备了"百万并发"的能力。本文深入解读虚拟线程的使用方式和底层实现原理。
一、为什么需要虚拟线程?
1.1 传统线程的痛点
在 Java 中,线程(Thread)一直是并发的基础单位。但传统线程有几个严重问题:
java
// 传统线程池
ExecutorService executor = Executors.newFixedThreadPool(100);
// 问题1:线程是重量级资源
// - 每个线程占用 1MB 左右的栈内存
// - 1000 个线程 = 1GB 内存
// - 创建/销毁线程需要 OS 调度
// 问题2:阻塞代价高
// 当线程执行 I/O 操作(数据库查询、HTTP 请求)时,线程会阻塞
// 但这个线程仍然占用内存和 OS 资源传统线程的困境:
| 场景 | 线程数 | 内存占用 | 效果 |
|---|---|---|---|
| 1000 用户聊天 | 1000 线程 | ~1GB | 内存爆炸 |
| 10000 请求/秒 | 10000 线程 | ~10GB | 直接 OOM |
| 等待 I/O | 阻塞占用 | 浪费资源 | 效率低下 |
1.2 虚拟线程的出现
Java 21 引入了虚拟线程(Virtual Thread),专门解决 I/O 密集型场景的并发问题:
java
// 虚拟线程:轻量级、高并发
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 1000000 个虚拟线程 = 1GB 内存(理论上)
// 阻塞时不占用 OS 线程虚拟线程的核心优势:
- 轻量级:每个虚拟线程仅占用几 KB 内存
- 高并发:支持数十万甚至百万级并发
- 低成本阻塞:I/O 阻塞时释放载体线程
二、虚拟线程的使用方式
2.1 基本创建方式
java
public class VirtualThreadDemo {
public static void main(String[] args) {
// 方式1:Thread.ofVirtual() 创建
Thread vt1 = Thread.ofVirtual().name("my-vt-1").start(() -> {
System.out.println("虚拟线程运行中: " + Thread.currentThread());
});
// 方式2:Thread.startVirtualThread() 便捷方法
Thread vt2 = Thread.startVirtualThread(() -> {
System.out.println("快捷方式创建的虚拟线程");
});
// 方式3:Executors.newVirtualThreadPerTaskExecutor()
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 提交 10000 个任务
for (int i = 0; i < 10000; i++) {
final int taskId = i;
executor.submit(() -> {
// 模拟 I/O 操作
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " 完成");
});
}
executor.shutdown();
}
}2.2 与 ThreadPoolExecutor 对比
java
public class ThreadComparison {
// 传统线程池
private static void traditionalThreadPool() {
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
final int taskId = i;
executor.submit(() -> {
try {
// 模拟 I/O 等待
Thread.sleep(100);
} catch (InterruptedException e) {}
System.out.println("Task " + taskId);
});
}
executor.shutdown();
}
// 虚拟线程
private static void virtualThreadPool() {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 1000; i++) {
final int taskId = i;
executor.submit(() -> {
try {
// 模拟 I/O 等待
Thread.sleep(100);
} catch (InterruptedException e) {}
System.out.println("Task " + taskId);
});
}
executor.shutdown();
}
}性能对比:
| 指标 | 传统线程池 (100) | 虚拟线程 (1000) |
|---|---|---|
| 内存占用 | ~500MB | ~50MB |
| 吞吐量 | 100 req/s | 10000 req/s |
| 线程切换 | 高开销 | 无开销 |
2.3 在 Spring Boot 中使用
java
@Configuration
public class VirtualThreadConfig {
@Bean
public Executor virtualThreadExecutor() {
// Spring Boot 3.2+ 支持
return Executors.newVirtualThreadPerTaskExecutor();
}
}
// 使用示例
@Service
public class AsyncService {
@Async // 自动使用虚拟线程(Spring Boot 3.2+)
public void processAsync() {
// 异步执行
}
// 或者手动使用
@Autowired
private Executor virtualThreadExecutor;
public void manualVirtualThread() {
virtualThreadExecutor.execute(() -> {
// 虚拟线程执行
});
}
}2.4 虚拟线程的局限性
java
public class VirtualThreadLimitations {
public static void main(String[] args) {
// 不适合 CPU 密集型任务
ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();
vt.submit(() -> {
// 虚拟线程不适合 CPU 密集型!
// 因为虚拟线程需要载体线程执行,CPU 密集会占满载体线程
long sum = 0;
for (long i = 0; i < 1_000_000_000L; i++) {
sum += i;
}
return sum;
});
// 适合 I/O 密集型任务
vt.submit(() -> {
// 数据库查询
// HTTP 请求
// 文件读写
// 虚拟线程释放载体线程,其他虚拟线程可以使用
});
// 注意:虚拟线程不能使用 ThreadLocal
// ThreadLocal.withInitial(() -> "value"); // 不适用
// 但可以使用 ScopedValue(Java 21 新特性)
static final ScopedValue<String> USER_INFO = ScopedValue.newInstance();
ScopedValue.where(USER_INFO, "user123").run(() -> {
// 在这个作用域内可以获取
System.out.println(USER_INFO.get());
});
}
}三、虚拟线程的底层原理
3.1 核心概念:载体线程
虚拟线程的原理可以理解为"不绑定 OS 线程的轻量级任务":
关键点:
- 虚拟线程(Virtual Thread, VT)是用户态线程
- 载体线程(Carrier Thread)是实际的 OS 线程
- 多个 VT 共享少量载体线程
- VT 阻塞时,载体线程被释放给其他 VT 使用
3.2 Continuation(延续)
虚拟线程的核心实现基于 Continuation(延续):
java
// Continuation 伪代码实现
public class Continuation {
private final Stack stack; // 栈帧数据
private State state; // RUNNING / SUSPENDED
// 暂停(yield)
public static void yield() {
// 1. 保存当前栈帧到堆
saveStack(currentThread.continuation);
// 2. 切换到调度器
switchToScheduler();
}
// 恢复(resume)
public static void resume(Continuation cont) {
// 1. 从堆恢复栈帧
restoreStack(cont.stack);
// 2. 继续执行
continueExecution(cont);
}
}Continuation 的作用:
3.3 调度器(Scheduler)
虚拟线程的调度器默认是 ForkJoinPool:
java
// 虚拟线程的默认调度器
public class VirtualThreadScheduler {
// ForkJoinPool 作为默认调度器
private static final ForkJoinPool DEFAULT_SCHEDULER =
ForkJoinPool.commonPool();
public static void main(String[] args) {
// 查看默认调度器
System.out.println(ForkJoinPool.commonPool());
// 虚拟线程使用调度器
Thread vt = Thread.startVirtualThread(() -> {
// 实际上由 ForkJoinPool 的载体线程执行
});
}
}调度流程:
3.4 内存模型
虚拟线程将栈内存从堆外移到堆内:
java
public class StackMemoryDemo {
public static void main(String[] args) throws Exception {
// 传统线程栈:在 OS 分配,约 1MB/线程
// 虚拟线程栈:在 JVM 堆分配,约几百 KB/线程
// 模拟查看虚拟线程的栈大小
Thread vt = Thread.startVirtualThread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
vt.join();
// 虚拟线程栈内存实际上是动态的
// - 初始:几百 KB
// - 最大:可以根据需要扩展
}
}内存对比:
| 类型 | 栈位置 | 默认大小 | 最大 | 10万线程总内存 |
|---|---|---|---|---|
| 传统线程 | OS 堆外 | 1MB | 1MB | ~1GB |
| 虚拟线程 | JVM 堆内 | 512KB | 1GB | ~50MB |
3.5 阻塞检测与切换
虚拟线程能够"感知"阻塞操作并自动切换:
java
public class BlockingDetection {
public static void main(String[] args) {
Thread.startVirtualThread(() -> {
// 这些操作会让虚拟线程让出载体线程
// 1. Thread.sleep()
try {
Thread.sleep(1000); // 阻塞 → yield → 释放载体
} catch (InterruptedException e) {}
// 2. Object.wait()
synchronized (new Object()) {
try {
new Object().wait(); // 阻塞
} catch (InterruptedException e) {}
}
// 3. LockSupport.park()
LockSupport.parkNanos(1000000000); // 阻塞
// 4. I/O 操作(底层使用上述机制)
// new URL("...").openConnection().getInputStream();
// 不阻塞的操作
// - CAS 操作 (AtomicInteger)
// - 纯计算
});
}
}四、实战:虚拟线程最佳实践
4.1 适用场景
java
public class BestPractice {
// 适合使用虚拟线程的场景
// 1. HTTP 服务端
@GetMapping("/api/users")
public List<User> getUsers() {
// 每个请求一个虚拟线程
// 10000 并发请求 = 10000 虚拟线程 = 少量载体线程
return userService.findAll();
}
// 2. 消息消费者
@JmsListener(queue = "orders")
public void processOrder(Order order) {
// 每个消息一个虚拟线程
orderProcessor.process(order);
}
// 3. 批量处理
public void batchProcess(List<Item> items) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
items.forEach(item ->
executor.submit(() -> processItem(item))
);
}
}
// 不适合使用虚拟线程的场景
// 1. CPU 密集型计算
public void cpuHeavy() {
// 虚拟线程会被 CPU 密集任务占满载体线程
// 应该用传统线程池
}
// 2. 需要使用 ThreadLocal 的地方
public void threadLocalUsage() {
// 虚拟线程不支持 ThreadLocal
// 改用 ScopedValue (Java 21+)
}
}4.2 性能调优
java
public class PerformanceTuning {
public static void main(String[] args) {
// 1. 控制并发数(信号量)
Semaphore semaphore = new Semaphore(10000);
ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100000; i++) {
semaphore.acquire();
vt.submit(() -> {
try {
// 业务逻辑
} finally {
semaphore.release();
}
});
}
// 2. 避免创建过多虚拟线程
// 虚拟线程虽然轻量,但也不是无限
// 建议:并发数 = 10000 ~ 100000
// 3. 使用结构化并发(Java 21)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var future1 = scope.fork(() -> task1());
var future2 = scope.fork(() -> task2());
scope.join();
scope.throwIfFailed();
// 结果自动合并
}
}
}4.3 调试技巧
java
public class DebugTips {
public static void main(String[] args) {
// 1. 查看虚拟线程
Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.forEach(System.out::println);
// 2. 虚拟线程命名
Thread vt = Thread.ofVirtual()
.name("worker-", 0) // worker-0, worker-1, ...
.start(() -> {});
// 3. 调试时查看载体线程
System.out.println(Thread.currentThread());
// 输出类似: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-3
// 表示:在 ForkJoinPool-1 的 worker-3 上运行的虚拟线程 #30
}
}五、总结
5.1 虚拟线程核心要点
| 特性 | 说明 |
|---|---|
| 引入版本 | Java 21 (预览版 Java 19/20) |
| 创建方式 | Thread.ofVirtual(), Executors.newVirtualThreadPerTaskExecutor() |
| 适用场景 | I/O 密集型任务(HTTP、数据库、文件等) |
| 不适用场景 | CPU 密集型任务、需要 ThreadLocal 的场景 |
| 内存模型 | 栈在堆内,动态扩展 |
| 阻塞处理 | 自动 yield 释放载体线程 |
| 调度器 | ForkJoinPool |
虚拟线程是 Java 继泛型、Lambda 之后最重要的特性。它让 Java 具备了"百万并发"的能力,是构建高并发系统的利器。掌握虚拟线程,将成为 Java 开发者的必备技能。