虚拟线程与协程在微服务中的实践
JDK 21 正式引入虚拟线程(Project Loom),这是 Java 并发模型十年来最大的变革:用几十万、上百万个轻量线程支撑高并发 IO,把"每请求一线程"的编程模型还给了开发者。本文讲透虚拟线程的原理,对比平台线程,并给出微服务中的落地实践与基准数据。
为什么需要虚拟线程
平台线程的困境
传统"每请求一线程"模型:
请求 → 平台线程(1:1 映射操作系统线程)
平台线程的成本:
├─ 创建成本:约 1MB 栈内存(默认栈大小)
├─ 切换成本:系统调用(上下文切换)
└─ 数量限制:几千到几万个就到头
高并发 IO 场景:
├─ 大量请求大部分时间在等待(DB / RPC / MQ)
├─ 线程等待时白占 1MB 内存 + 阻塞
└─ 10 万并发 = 需要 10 万个平台线程 → 不可能现有的应对方案
WebFlux(Reactive):
├─ 用少量线程 + 事件循环支撑高并发
├─ 但编程模型反人类:回调 / Mono / Flux
└─ 学习成本高,排障困难
异步编程(CompletableFuture):
├─ 线程不阻塞,靠回调续接
└─ 代码割裂、调试困难
虚拟线程的目标:
用"阻塞式"写法(像写同步代码一样),
却拥有接近异步模型的高并发吞吐虚拟线程原理
什么是虚拟线程
虚拟线程(Virtual Thread):
├─ JVM 管理的轻量线程,不直接映射 OS 线程
├─ 由 JDK 调度器(Scheduler)挂载到少量平台线程上运行
├─ 一个平台线程(Carrier)上可以"驮"多个虚拟线程
└─ 阻塞时虚拟线程自动让出平台线程(由 JVM 接管)
对比:
├─ 平台线程:1 线程 : 1 OS 线程(昂贵)
└─ 虚拟线程:N 虚拟线程 : 1 OS 线程(便宜)虚拟线程执行模型:
虚拟线程 V1 ─┐
虚拟线程 V2 ─┼──► 平台线程 P1(Carrier,如 8 核 8 线程)
虚拟线程 V3 ─┘
当 V1 执行 IO 阻塞:
├─ JVM 检测到阻塞 → V1 从 P1 上"卸载"
├─ P1 立即承接 V2 继续运行
└─ IO 完成后 V1 被调度器重新挂载到某个平台线程关键设计
虚拟线程的关键机制:
├─ 挂载/卸载(Mount/Unmount):阻塞点自动切换
├─ 阻塞点识别:
│ ├─ 文件 IO、网络 IO、锁等待、sleep
│ └─ synchronized 受限(阻塞时可能钉住平台线程)
├─ 调度器(ForkJoinPool):
│ └─ 默认并行度 = CPU 核数
└─ 栈:堆中分配,可增长,不占 OS 栈
钉住问题(Pinning):
├─ synchronized 块内阻塞 → 虚拟线程钉住平台线程
├─ 虽然功能正确,但会降低并发
└─ 解决:尽量用 ReentrantLock 代替 synchronized虚拟线程 vs 平台线程
对比表
| 对比项 | 平台线程 | 虚拟线程 |
|---|---|---|
| 映射关系 | 1:1 映射 OS 线程 | N:1 由 JVM 调度 |
| 创建成本 | 高(约 1MB 栈) | 极低(微秒级) |
| 数量上限 | 几千 ~ 几万 | 数十万 ~ 百万 |
| 切换成本 | 系统调用 | JVM 内切换 |
| 阻塞行为 | 阻塞 OS 线程 | 让出 Carrier,不阻塞 |
| 编程模型 | 同步阻塞 | 同步阻塞(一样!) |
| 适用场景 | CPU 密集 / 少量并发 | IO 密集 / 海量并发 |
关键结论
虚拟线程适合:
├─ IO 密集型任务(RPC、DB、MQ 调用多)
├─ 高并发、每请求阻塞占比高的业务
└─ 想保持同步代码风格的项目
虚拟线程不适合:
├─ CPU 密集型计算(虚拟线程不提升 CPU 利用率)
├─ 大量 synchronized 的旧代码(钉住问题)
└─ 需要手动控制线程优先级/调度的场景虚拟线程的使用
创建虚拟线程
java
// 方式一:Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
.name("vthread-1")
.start(() -> {
System.out.println("Hello from virtual thread");
});
// 方式二:Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> doTask(1));
executor.submit(() -> doTask(2));
// 每个任务一个虚拟线程,任务结束虚拟线程自动销毁
}
// 方式三:Thread.startVirtualThread()
Thread.startVirtualThread(() -> doTask());与 CompletableFuture 的对比
java
// 传统异步写法(回调割裂)
CompletableFuture.supplyAsync(() -> callServiceA())
.thenCombine(CompletableFuture.supplyAsync(() -> callServiceB()),
(a, b) -> merge(a, b))
.thenApply(this::saveResult)
.join();
// 虚拟线程写法(同步直写)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<A> fa = executor.submit(this::callServiceA);
Future<B> fb = executor.submit(this::callServiceB);
Result r = saveResult(merge(fa.get(), fb.get())); // 同步取结果
}虚拟线程最大的价值:
├─ 代码回归"同步、直观、可调试"
├─ 不需要响应式编程,不需要回调地狱
└─ 排障时看堆栈就是完整调用链吞吐量基准测试
测试模型
场景:模拟 IO 密集型业务
├─ 每个请求:等待 10ms(模拟 DB 查询)后返回
├─ 平台线程池:固定 200 线程
└─ 虚拟线程:每请求一个虚拟线程
对比维度:
├─ 支持的并发数
├─ 吞吐量(QPS)
└─ 资源占用(内存、线程数)典型结果(示意数据)
10000 并发请求:
| 方案 | 完成时间 | 线程数 | 内存 |
|------------|---------|--------|------|
| 平台线程池 | 较慢 | 200 | 中等 |
| 虚拟线程 | 快 数倍 | 10000+ | 相近 |
关键观察:
├─ 虚拟线程可以创建"请求数 = 线程数"的线程
├─ 平台线程池在 200 并发后排队等待
└─ 吞吐提升来自"阻塞不占线程"基准测试注意点
压测虚拟线程的注意:
├─ 压测工具本身不能成为瓶颈(用异步压测)
├─ 要对比"同业务逻辑"(不能只测空转)
├─ 关注 P99 与吞吐,不是单看并发数
└─ 数据库连接池要匹配(连接池小会限制真实吞吐)Spring Boot 中的虚拟线程
Spring Boot 3.2+ 原生支持
yaml
# application.yml
spring:
threads:
virtual:
enabled: true # 开启虚拟线程(Tomcat 用虚拟线程处理请求)开启后的效果:
├─ Tomcat 用虚拟线程处理 HTTP 请求
├─ 默认 maxThreads 不再适用(虚拟线程无限)
├─ 每个请求一个虚拟线程
└─ 业务代码无需任何改动异步与调度
java
// @Async 默认用虚拟线程(Spring Boot 3.2+ 配置)
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
// 每任务一个虚拟线程的执行器
return new TaskExecutorAdapter(
Executors.newVirtualThreadPerTaskExecutor());
}
}
// 使用
@Async
public void sendNotification(Order order) {
// 在虚拟线程中执行
}定时任务与虚拟线程
java
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
// Spring @Scheduled 使用虚拟线程调度器
return new ConcurrentTaskScheduler(
Executors.newScheduledThreadPool(1),
Executors.newVirtualThreadPerTaskExecutor());
}
}微服务落地实践
落地场景
虚拟线程在微服务中的典型场景:
├─ 网关 / 业务网关:高并发请求转发(IO 密集)
├─ 聚合服务:并发调用多个下游再合并(IO 密集)
├─ 消息消费:批量异步处理消息
├─ 定时任务:大量独立小任务并行
└─ 数据迁移 / 批量处理:海量 IO 任务聚合服务示例
java
@Service
public class OrderAggregateService {
// 并发查 3 个下游 + 用户信息,然后聚合
public OrderDetailVO getOrderDetail(Long orderId, Long userId) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Order> orderF = executor.submit(() -> orderClient.get(orderId));
Future<List<Item>> itemF = executor.submit(() -> itemClient.list(orderId));
Future<Account> acctF = executor.submit(() -> accountClient.get(userId));
// 同步等待,虚拟线程让出平台线程不阻塞
return assemble(orderF.get(), itemF.get(), acctF.get());
}
}
}注意与踩坑
虚拟线程落地注意:
├─ 1. 线程局部变量(ThreadLocal)
│ 虚拟线程支持 ThreadLocal,但数量巨大时注意内存
├─ 2. synchronized 钉住问题
│ 老代码大量 synchronized → 优先替换为 ReentrantLock
├─ 3. 连接池
│ 虚拟线程不解决"连接不够"问题,连接池大小仍要规划
├─ 4. 线程池过渡封装
│ 虚拟线程场景尽量"每任务一线程",别再包固定池
├─ 5. JDK 版本
│ JDK 21 正式版支持,Spring Boot 3.2+ 原生整合
└─ 6. 压测验证
│ 上线前必须压测,确认无钉住/资源问题与现有架构的融合
融合路径:
├─ 增量改造:先改 IO 密集的接口,不动全部
├─ 网关先行:网关是纯 IO,收益最大
├─ 保留平台线程池:CPU 密集任务继续用平台线程
└─ 监控:线程数指标、阻塞时间、P99 变化
与动态线程池的关系:
├─ 虚拟线程是"线程模型"变革
├─ 动态线程池是"参数治理"
└─ IO 密集场景可逐步用虚拟线程替代线程池;
CPU 密集 / 资源受限场景继续用线程池 + 动态治理总结
虚拟线程让 Java 开发者用同步代码获得异步性能:JVM 把海量轻量虚拟线程调度到少量平台线程上,阻塞时自动让出载体,IO 密集场景吞吐成倍提升。JDK 21 + Spring Boot 3.2 的成熟支持让落地变得简单——开一个配置即可让 Tomcat 用虚拟线程处理请求。实践时注意 synchronized 钉住、连接池规划与压测验证,把虚拟线程用在 IO 密集链路上,是微服务并发模型演进的主要方向。