
Java 虚拟线程:协程概念与性能提升详解
Java 虚拟线程是 Java 21 引入的并发模型,基于 Project Loom。相比传统平台线程,虚拟线程在用户态调度,内存占用极低,支持百万级并发。主要优势包括轻量级栈、非绑定调度及自动挂起恢复机制。适用于 I/O 密集型任务如 Web 服务器和数据库连接池,可显著提升吞吐量和降低延迟。CPU 密集型任务仍建议使用平台线程。迁移成本低,对大多数代码透明。

Java 虚拟线程是 Java 21 引入的并发模型,基于 Project Loom。相比传统平台线程,虚拟线程在用户态调度,内存占用极低,支持百万级并发。主要优势包括轻量级栈、非绑定调度及自动挂起恢复机制。适用于 I/O 密集型任务如 Web 服务器和数据库连接池,可显著提升吞吐量和降低延迟。CPU 密集型任务仍建议使用平台线程。迁移成本低,对大多数代码透明。


Java 已经引入了协程的概念,协程在 I/O 密集型场景中可比传统线程提升 10-70 倍的吞吐量。这种性能飞跃主要源于协程的用户态调度、极低的内存占用和高效的上下文切换机制。
协程 (Coroutine) 与线程 (Thread) 在 Java 中代表两种不同的并发执行模型,它们的核心区别体现在调度机制、资源占用和执行方式上。
在调度方式上,线程由操作系统内核调度,采用抢占式机制,线程切换需要从用户态进入内核态,涉及复杂的上下文保存和恢复,每次切换耗时约 1-10 微秒。而协程完全在用户态执行和调度,采用协作式机制,协程在遇到阻塞操作时主动挂起,将 CPU 让给其他协程,切换成本降至 0.1-1 微秒,开销仅为线程的 1/10 至 1/100。
资源占用方面差异显著:传统 Java 线程在 64 位 Linux 系统上默认栈大小为 1MB,加上内核数据结构额外消耗约 16KB 内存。相比之下,协程的栈空间通常在几百字节到几 KB 之间,内存效率提升约 1000 倍。这意味着在同样内存条件下,协程可支持的并发量远超线程,如 Project Loom 测试显示单机可支撑百万级协程,而传统线程通常受限于数千个的并发量。
阻塞影响上也存在本质区别:线程阻塞(如 I/O 操作)会导致整个线程处于等待状态,无法处理其他任务,造成 CPU 资源浪费。协程则在遇到阻塞操作时主动挂起,释放线程资源,允许线程继续执行其他协程,大大提高了 CPU 利用率。例如在 Web 服务器场景中,协程可在等待数据库响应时让出线程,处理其他 HTTP 请求,而线程模型则需为每个请求分配独立线程。
并发规模方面,协程支持更高的并发级别:协程可轻松创建数万个甚至百万个实例,而传统线程模型在高并发时会面临内存耗尽和调度开销过大的问题。例如,材料显示 100 万个协程可正常运行,而同等数量的线程会导致系统直接卡死。
| 版本 | 状态 | 主要特性 |
|---|---|---|
| Java 19 | 预览功能 | 首次引入虚拟线程(JEP 425) |
| Java 20 | 第二次预览 | 优化 API 和性能(JEP 436) |
| Java 21 (LTS) | 正式发布 | 成为标准功能(JEP 444) |
| Java 22+ | 持续优化 | 性能提升和功能增强 |
// 传统线程(平台线程)
Thread platformThread = new Thread(() -> {
// 直接映射到操作系统线程
});
platformThread.start();
// 虚拟线程
Thread virtualThread = Thread.ofVirtual()
.name("virtual-thread-1")
.start(() -> {
// 由 JVM 调度,不直接绑定 OS 线程
});
内存对比表:
| 线程类型 | 栈大小 | 10K 线程内存 | 1M 线程内存 |
|---|---|---|---|
| 平台线程 | 1MB | ~10GB | ~1TB |
| 虚拟线程 | 400B-1KB | ~4-8MB | ~400-800MB |
基准测试结果:
// 创建性能测试 Benchmark Mode Cnt Score Error Units
// 创建 10,000 个线程
ThreadCreation.platformThread avgt 5 1250.234 ± 45.678 ms
ThreadCreation.virtualThread avgt 5 12.567 ± 0.789 ms
// 创建速度快约 100 倍
// 上下文切换测试
ContextSwitch.platformSwitch avgt 5 1500.00 ± 200.00 ns
ContextSwitch.virtualSwitch avgt 5 50.00 ± 10.00 ns
// 切换速度快约 30 倍
public class IOBenchmark {
// 模拟 HTTP 请求(每个请求 100ms 延迟)
private static void mockHttpRequest() {
try {
Thread.sleep(100);
} catch (Exception e) {}
}
public static void main(String[] args) throws Exception {
int requestCount = 10_000;
int threadPoolSize = 200;
// 平台线程池限制
// 1. 平台线程池方式
long start1 = System.currentTimeMillis();
try (ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize)) {
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < requestCount; i++) {
futures.add(executor.submit(IOBenchmark::mockHttpRequest));
}
for (Future<?> f : futures) f.get();
}
long time1 = System.currentTimeMillis() - start1;
System.out.println("平台线程池:" + time1 + "ms");
// 理论最短时间:(10000/200) * 100ms = 5000ms
// 实际结果:5000-6000ms
// 2. 虚拟线程方式
long start2 = System.currentTimeMillis();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < requestCount; i++) {
futures.add(executor.submit(IOBenchmark::mockHttpRequest));
}
for (Future<?> f : futures) f.get();
}
long time2 = System.currentTimeMillis() - start2;
System.out.println("虚拟线程:" + time2 + "ms");
// 理论最短时间:100ms + 调度开销
// 实际结果:100-200ms
}
}
性能提升总结:
// JVM 内部的调度机制
public class VirtualThreadScheduler {
// 载体线程池(ForkJoinPool)
private static final ForkJoinPool CARRIER_POOL = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
// 调度过程示意
public void scheduleVirtualThread(VirtualThread vt) {
// 1. 虚拟线程就绪
// 2. 从载体池获取线程执行
// 3. 遇到阻塞操作时:
// a. 保存栈状态到堆
// b. 挂起虚拟线程
// c. 释放载体线程给其他虚拟线程
// 4. I/O 完成时恢复执行
}
}
// 挂起时保存的栈帧信息
class VirtualThreadContinuation {
// 栈帧保存在堆上(小而紧凑)
private byte[] stackChunk;
private int stackPointer;
// 挂起:保存现场
void yield() {
saveStackToHeap();
returnToScheduler();
}
// 恢复:还原现场
void run() {
restoreStackFromHeap();
continueExecution();
}
}
// 方式 1:工厂方法
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("Running in virtual thread");
});
// 方式 2:构建器模式
Thread.ofVirtual()
.name("worker-", 0) // 名称和起始编号
.unstarted(task) // 不自动启动
.start();
// 方式 3:ExecutorService
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> future = executor.submit(() -> "result");
String result = future.get();
}
// 方式 4:使用线程工厂
ThreadFactory virtualThreadFactory = Thread.ofVirtual().factory();
ExecutorService executor = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 创建子任务
Future<String> userTask = scope.fork(() -> fetchUser());
Future<Integer> orderTask = scope.fork(() -> fetchOrder());
// 等待所有任务完成
scope.join();
// 检查失败
scope.throwIfFailed();
// 获取结果
String user = userTask.resultNow();
Integer order = orderTask.resultNow();
}
// 作用域结束自动取消未完成的任务
public class VirtualThreadOptimization {
// 1. 避免线程本地变量滥用
// 不好的做法:大量虚拟线程使用 ThreadLocal
private static final ThreadLocal<BigObject> tl = new ThreadLocal<>();
// 好的做法:使用 ScopedValue(Java 20+)
private static final ScopedValue<String> CONTEXT = ScopedValue.newInstance();
// 2. 避免 synchronized 阻塞(会固定载体线程)
public void process() {
// 避免:
// synchronized(this) { // 会 pin 住虚拟线程 // 长时间操作 }
// 推荐:使用 ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// 操作
} finally {
lock.unlock();
}
}
// 3. 合理配置载体线程数
public void configureCarrierThreads() {
// 默认 = Runtime.getRuntime().availableProcessors()
System.setProperty(
"jdk.virtualThreadScheduler.parallelism", "32"
);
// 最大池大小
System.setProperty(
"jdk.virtualThreadScheduler.maxPoolSize", "256"
);
}
}
// 使用虚拟线程的 Web 服务器 vs 传统线程池
@SpringBootApplication
public class WebServerBenchmark {
// Tomcat 配置虚拟线程
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
// 测试结果对比
/*
硬件:4 核 CPU,16GB 内存
测试工具:wrk - 1000 并发连接
传统线程池(200 线程):
- 吞吐量:3,200 req/sec
- 延迟 P95:450ms
- CPU 使用率:65%
- 内存使用:1.2GB
虚拟线程:
- 吞吐量:12,800 req/sec (4 倍提升)
- 延迟 P95:95ms (4.7 倍降低)
- CPU 使用率:85% (更高利用率)
- 内存使用:350MB (70% 减少)
*/
}
// 传统方式:固定线程池
@Configuration
public class TraditionalConfig {
@Bean
public ExecutorService taskExecutor() {
// 通常限制在 50-200 个线程
return Executors.newFixedThreadPool(100);
}
// 瓶颈:每个线程持有一个数据库连接
// 100 个线程 → 最多 100 个并发查询
}
// 虚拟线程方式
@Configuration
public class VirtualThreadConfig {
@Bean
public ExecutorService taskExecutor() {
// 每个任务独立虚拟线程
return Executors.newVirtualThreadPerTaskExecutor();
}
// 优势:虚拟线程在等待数据库响应时挂起
// 可以处理数千个并发查询,连接池大小不变
}
public class UnsuitableScenarios {
// 1. CPU 密集型任务
public void cpuIntensiveTask() {
// 虚拟线程不会带来收益
// 甚至可能因调度开销而略慢
// 计算质数、图像处理、复杂算法等
// 应使用平台线程或 ForkJoinPool
}
// 2. 依赖 ThreadLocal 的遗留代码
public void legacyCode() {
// 大量虚拟线程 + ThreadLocal = 内存爆炸
ThreadLocal<byte[]> largeBuffer = new ThreadLocal<>();
// 推荐迁移到 ScopedValue
}
}
// 监控虚拟线程
public class VirtualThreadMonitoring {
public static void main(String[] args) {
// 启用详细日志
System.setProperty(
"jdk.traceVirtualThreadLevel", "1"
// 0=off, 1=info, 2=debug
);
// 使用 JFR 监控
// jcmd <pid> JFR.start name=vtrace filename=vtrace.jfr
// jcmd <pid> JFR.dump name=vtrace
// 线程转储包含虚拟线程
Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.forEach(System.out::println);
}
}
虚拟线程是 Java 并发模型的革命性改进,特别适合现代云原生和微服务架构,能够显著降低资源消耗同时提高应用吞吐量。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online