虚拟线程与现代并发编程
传统线程模型的困境
在探讨虚拟线程之前,我们先来理解传统Java线程模型面临的挑战。
操作系统线程的三种实现方式
操作系统级别的线程实现主要有三种方式:
内核线程实现(1:1模型):每个用户线程直接映射到一个内核线程,由操作系统内核负责调度。优点是充分利用多核处理器,缺点是创建、销毁和上下文切换的成本高。
用户线程实现(N:1模型):多个用户线程映射到一个内核线程,线程调度完全在用户空间完成。优点是轻量高效,缺点是无法利用多核优势,一个线程阻塞会导致所有线程阻塞。
混合实现(M:N模型):多个用户线程映射到少量内核线程,兼具前两种方式的优点,但实现复杂度高。
graph TB
subgraph 1:1内核线程模型
A1[用户线程1] --> B1[内核线程1]
A2[用户线程2] --> B2[内核线程2]
A3[用户线程3] --> B3[内核线程3]
end
subgraph M:N混合模型
C1[用户线程1] --> D1[内核线程1]
C2[用户线程2] --> D1
C3[用户线程3] --> D2[内核线程2]
C4[用户线程4] --> D2
end
style B1 fill:#ffcdd2
style B2 fill:#ffcdd2
style B3 fill:#ffcdd2
style D1 fill:#c8e6c9
style D2 fill:#c8e6c9Java平台线程的局限性
在JDK 21之前,Java主要采用1:1的线程模型,每个Java线程都对应一个操作系统的内核线程(在Windows和Linux等主流平台上)。这种设计虽然简单直接,但存在明显的限制:
创建成本高:创建线程需要分配内核资源,调用系统API,开销较大。
上下文切换开销:线程切换需要在用户态和内核态之间切换,保存和恢复寄存器状态,消耗CPU资源。
内存占用大:每个线程都需要分配独立的栈空间(通常1MB左右),大量线程会占用大量内存。
数量受限:受操作系统和硬件资源限制,单个JVM能创建的线程数量有上限(通常几千到几万)。
这些限制在高并发场景下尤为突出。例如,一个需要处理百万级并发连接的服务器,无法为每个连接分配一个平台线程。
虚拟线程的革命性创新
协程思想的引入
虚拟线程(Virtual Thread)的概念对于熟悉Go、Python、Ruby等语言的开发者来说并不陌生——这就是协程(Coroutine)。
JDK 21正式引入虚拟线程,从根本上改变了Java的并发编程模式。虚拟线程是JVM实现的轻量级线程,它将多个虚拟线程映射到少量操作系统线程上,通过智能调度避免了传统线程的高昂开销。
M:N调度模型
虚拟线程采用M:N调度模型:
- M个虚拟线程(数量可以非常大,百万级别)
- N个载体线程(Carrier Thread,即平台线程,数量较少)
- JVM调度器:负责将虚拟线程调度到载体线程上执行
graph TB
subgraph 虚拟线程层
V1[虚拟线程1]
V2[虚拟线程2]
V3[虚拟线程3]
V4[虚拟线程4]
V5[虚拟线程5]
V6[虚拟线程...]
end
subgraph JVM调度器
S[智能调度<br/>避免上下文切换]
end
subgraph 载体线程平台线程
P1[平台线程1]
P2[平台线程2]
end
subgraph 操作系统
K1[内核线程1]
K2[内核线程2]
end
V1 --> S
V2 --> S
V3 --> S
V4 --> S
V5 --> S
V6 --> S
S --> P1
S --> P2
P1 --> K1
P2 --> K2
style S fill:#fff9c4
style P1 fill:#c8e6c9
style P2 fill:#c8e6c9
style V1 fill:#e1f5fe
style V2 fill:#e1f5fe
style V3 fill:#e1f5fe
style V4 fill:#e1f5fe
style V5 fill:#e1f5fe
style V6 fill:#e1f5fe核心优势
极低的创建成本:创建虚拟线程只需在堆上分配一个Java对象,无需系统调用,成本极低。
高效的内存使用:虚拟线程的栈是动态伸缩的,初始只有几KB,按需增长,内存占用远小于平台线程。
无上下文切换开销:虚拟线程的切换在用户空间完成,不涉及内核态切换,效率极高。
数量几乎无限:可以轻松创建百万级别的虚拟线程,只受JVM堆内存限制。
简化异步编程:可以用同步代码风格编写异步逻辑,无需复杂的回调和异步框架。
虚拟线程的使用方式
Thread.ofVirtual()方式
使用Thread.ofVirtual()创建虚拟线程:
// 创建并启动虚拟线程
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("虚拟线程执行中:" + Thread.currentThread());
});
// 等待虚拟线程完成
vThread.join();Thread.startVirtualThread()快捷方式
更简洁的方式是使用Thread.startVirtualThread():
Thread.startVirtualThread(() -> {
System.out.println("快速创建虚拟线程");
// 执行业务逻辑
processTask();
});Builder模式创建
使用Builder模式可以设置线程名称等属性:
Thread.Builder virtualBuilder = Thread.ofVirtual().name("virtual-worker-", 0);
Thread vt1 = virtualBuilder.start(() -> processRequest("REQ-001"));
Thread vt2 = virtualBuilder.start(() -> processRequest("REQ-002"));
vt1.join();
vt2.join();对比平台线程的创建:
Thread.Builder platformBuilder = Thread.ofPlatform().name("platform-worker");
Thread pt = platformBuilder.start(() -> {
System.out.println("平台线程执行:" + Thread.currentThread());
});虚拟线程池
JDK 21提供了专门为虚拟线程设计的Executor:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交大量任务
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
try {
Thread.sleep(Duration.ofMillis(100));
return handleTask(i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
});
});
} // 自动关闭,等待所有任务完成重要提示:虽然可以使用线程池管理虚拟线程,但这并不是推荐的做法。传统线程池的设计目的是复用昂贵的平台线程,而虚拟线程创建成本极低,使用线程池反而会增加复杂度。通常直接创建虚拟线程即可。
虚拟线程与平台线程的差异
守护线程特性
虚拟线程总是守护线程,无法通过setDaemon(false)改变。这意味着:
- JVM不会等待虚拟线程执行完毕才退出
- 当所有非守护的平台线程结束时,JVM会立即终止
- 虚拟线程中的未完成任务可能被强制中断
// 这段代码可能看不到输出
Thread.startVirtualThread(() -> {
try {
Thread.sleep(5000);
System.out.println("这行可能不会执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 主线程结束,JVM退出,虚拟线程被终止
// 正确做法:主线程等待
Thread vt = Thread.startVirtualThread(() -> {
try {
Thread.sleep(5000);
System.out.println("这行会执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
vt.join(); // 等待虚拟线程完成优先级限制
虚拟线程的优先级固定为Thread.NORM_PRIORITY(5),调用setPriority()无效:
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("优先级:" + Thread.currentThread().getPriority());
});
vt.setPriority(Thread.MAX_PRIORITY); // 无效操作
// 优先级仍然是5不支持的方法
虚拟线程不支持以下已过时的方法:
stop()suspend()resume()
调用这些方法会抛出UnsupportedOperationException异常。
ThreadLocal的使用限制
虽然虚拟线程支持ThreadLocal,但由于虚拟线程数量可能非常大,不当使用ThreadLocal可能导致严重的内存问题。
问题场景:
// 危险:可能创建百万个ThreadLocal实例
ThreadLocal<HeavyObject> threadLocal = ThreadLocal.withInitial(HeavyObject::new);
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
HeavyObject obj = threadLocal.get(); // 每个虚拟线程一个实例!
obj.process();
});
});
}推荐方案:使用Scoped Values(作用域变量)
JDK 21引入的Scoped Values(JEP 429)是ThreadLocal的现代替代方案,专门为虚拟线程设计:
// 定义作用域变量
final static ScopedValue<UserContext> USER_CONTEXT = ScopedValue.newInstance();
// 设置作用域变量(不可变)
void handleRequest(Request request) {
UserContext context = new UserContext(request.getUserId());
ScopedValue.where(USER_CONTEXT, context).run(() -> {
// 在此作用域内可以访问context
processBusinessLogic();
callOtherMethods();
});
// 离开作用域,变量自动清理
}
// 在其他方法中访问
void processBusinessLogic() {
UserContext context = USER_CONTEXT.get();
System.out.println("当前用户:" + context.getUserId());
}Scoped Values的优势:
- 不可变性:值一旦设置就不能修改,线程安全
- 作用域明确:变量只在特定代码块内有效,离开作用域自动清理
- 内存友好:相比ThreadLocal更适合大量虚拟线程的场景
性能对比实战
让我们通过实际测试对比虚拟线程和平台线程的性能差异。
测试场景设计
模拟IO密集型任务,每个任务执行简单计算后休眠10ms:
private static void simulateIOTask() {
try {
// 模拟计算
IntStream.range(0, 100).forEach(i -> {
Math.sqrt(i);
});
// 模拟IO阻塞
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}平台线程测试
使用传统线程池执行10000个任务:
private static void testPlatformThreads(int taskCount) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(200);
long startTime = System.nanoTime();
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
try {
simulateIOTask();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
long duration = (System.nanoTime() - startTime) / 1_000_000;
System.out.println("平台线程执行时间:" + duration + " ms");
}虚拟线程测试
使用虚拟线程执行相同任务:
private static void testVirtualThreads(int taskCount) throws InterruptedException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
long startTime = System.nanoTime();
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
try {
simulateIOTask();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
long duration = (System.nanoTime() - startTime) / 1_000_000;
System.out.println("虚拟线程执行时间:" + duration + " ms");
}性能测试结果
public static void main(String[] args) throws InterruptedException {
int taskCount = 10_000;
System.out.println("执行 " + taskCount + " 个任务");
System.out.println("================================");
testPlatformThreads(taskCount);
// 输出:平台线程执行时间:约550 ms
Thread.sleep(1000); // 间隔一下
testVirtualThreads(taskCount);
// 输出:虚拟线程执行时间:约120 ms
}测试结论:
- 平台线程(200个线程池):约550ms
- 虚拟线程:约120ms
- 性能提升约4.5倍
性能差距的主要原因:
- 平台线程数量受限(200个),任务需要排队等待
- 虚拟线程可以创建10000个,几乎无需等待
- 虚拟线程的上下文切换开销远小于平台线程
虚拟线程适用场景
适合使用虚拟线程
IO密集型应用:
- Web服务器处理大量HTTP请求
- 数据库密集型查询应用
- 文件IO操作
- 网络通信
高并发场景:
- 需要处理数十万并发连接的服务
- 消息处理系统
- 实时推送服务
简化异步编程:
- 用同步代码风格编写异步逻辑
- 替代复杂的异步框架和回调
不适合使用虚拟线程
CPU密集型任务:
- 大量数学计算
- 图像处理
- 加密解密
这类任务几乎不阻塞,虚拟线程的优势无法体现,反而增加调度开销。
固定使用ThreadLocal的老代码:
- 大量使用ThreadLocal的遗留系统
- 需要重构为Scoped Values
总结
虚拟线程是Java并发编程的重大进步,它:
- 采用M:N调度模型,将大量虚拟线程映射到少量平台线程
- 创建成本极低,可以支持百万级并发
- 在IO密集型场景下性能提升显著
- 简化了异步编程,用同步代码风格处理并发
使用虚拟线程的最佳实践:
- 优先选择IO密集型场景
- 避免滥用ThreadLocal,改用Scoped Values
- 注意虚拟线程是守护线程,主线程需等待
- 不要将虚拟线程放入传统线程池
- CPU密集型任务仍使用平台线程
随着JDK的持续演进,虚拟线程将成为Java并发编程的主流方案,帮助开发者构建更高性能、更易维护的并发应用。
更新: 2025-12-04 17:36:27
原文: https://www.yuque.com/u22210564/zoxfmt/doc-07-30