JDK21 虚拟线程 VS 传统线程
· 阅读需 5 分钟
JDK21虚拟线程的定义与核心特性
轻量级实现:虚拟线程(Virtual Threads)由JVM管理,与操作系统线程解耦,单应用可创建数百万个线程,解决了传统线程的资源限制问题。
核心优势对比:
-
资源消耗低:创建和销毁开销仅为平台线程的1/1000,适用于高并发场景。
-
高效调度:通过挂起机制减少上下文切换,I/O阻塞时自动释放载体线程(Carrier Thread)。
-
简化编程:语法与传统线程兼容,无需学习复杂并发框架。
JDK21虚拟线程的优势
使用虚拟线程(Virtual Threads)后,并不意味着可以完全抛弃传统线程池。二者各有适用场景,在实际开发中更可能是互补关系,而非替代关系。
| 特性 | 平台线程 | 虚拟线程 |
|---|---|---|
| 内存开销 | 约1MB/线程 | 初始4KB,弹性扩展 |
| 上下文切换 | 内核级,成本高 | 用户级,成本极低 |
| 阻塞处理 | 线程挂起,无法复用 | 自动挂起/恢复,资源可复用 |
| 调度方式 | 操作系统抢占式调度 | JVM协作式调度 |
| 适用场景 | CPU密集型任务 | IO密集型任务 |
为什么不能完全抛弃传统线程池?
1 CPU密集型任务仍需传统线程池
虚拟线程的优势体现在I/O密集型任务(线程大部分时间在等待I/O,而非占用CPU)。但对于CPU密集型任务(如复杂计算、数据处理):
- 此时线程会持续占用CPU,虚拟线程无法通过“挂起”释放资源(因为本质上还是需要OS线程执行计算)
- 过多的虚拟线程同时执行CPU密集型任务,会导致CPU上下文切换频繁,反而降低效率
- 传统线程池(如FixedThreadPool)通过限制线程数量(通常设为CPU核心数), 可避免CPU过载,更适合此类场景
2 资源隔离仍需传统线程池
传统线程池的核心价值之一是资源隔离(通过不同线程池隔离不同业务/任务):
- 例如:用一个线程池处理核心业务(如支付),另一个线程池处理非核心业务(如日志上报),避免非核心任务耗尽资源影响核心业务
- 虚拟线程池(如Executors.newVirtualThreadPerTaskExecutor())通常是全局共享的,难以实现精细化的资源隔离
- 复杂系统中,仍需传统线程池来控制特定任务的资源使用上限(如最大并发数、队列长度)
3 兼容性与迁移成本
- 现有系统中大量代码依赖传统线程池的特性(如ThreadPoolExecutor的拒绝策略、监控指标、线程命名规则等)
- 虚拟线程虽兼容Thread API,但与传统线程池的部分设计理念(如“池化复用”)不同,直接替换可能引入风险
- 混合使用两种线程模型(关键路径用虚拟线程提效,核心任务用传统线程池保稳定)是更务实的选择
典型混合使用案例
以一个电商系统为例:
// 1. 虚拟线程池:处理高并发I/O任务(如HTTP请求、数据库查询)
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
// 2. 传统线程池:处理CPU密集型任务(如订单金额计算、库存扣减)
ExecutorService cpuExecutor = new ThreadPoolExecutor(
***// 省略
);
// 处理用户下单请求(I/O密集型,用虚拟线程)
virtualExecutor.submit(() -> {
// 步骤1:查询商品信息(数据库I/O)
Product product = productDao.query(productId);
// 步骤2:计算订单金额(CPU密集型,提交给传统线程池)
BigDecimal amount = cpuExecutor.submit(() ->
orderCalculator.calculate(product, quantity)
).get();
// 步骤3:创建订单(数据库I/O)
orderDao.create(new Order(productId, amount));
});
分析
submit()本身是非阻塞的:它会立即返回一个Future<BigDecimal>。- 但
.get()是阻塞方法:它会一直等待,直到后台的 CPU 任务执行完毕并返回结果。 - 因为这段代码运行在 虚拟线程 中(由
virtualExecutor提交的任务),所以: - 虚拟线程会被 挂起(park),但底层的 载体线程(carrier thread)不会被阻塞(这是虚拟线程的优势)。
- 从 JVM 调度角度看,这是“轻量级阻塞”,性能开销小。
- 但从逻辑上讲,当前任务(下单流程)在步骤2是同步等待结果的,后续步骤(如创建订单)必须等金额计算完成后才能继续。
总结
虚拟线程是对Java并发模型的重要补充,而非替代传统线程池。在实际开发中:
- I/O密集型高并发场景:优先使用虚拟线程提高吞吐量
- CPU密集型或需要资源隔离的场景:继续使用传统线程池
- 大多数系统会混合使用两种模型,充分发挥各自优势
核心原则是:根据任务类型(I/O密集/CPU密集)和资源需求(高并发/隔离控制)选择合适的线程模型,而非盲目替换。
⚠️注意
JDK21虚拟线程有个弊端,synchronized修饰的方法和synchronized块要注意,不能长时间阻塞和频繁调用。 如果需长时间阻塞和频繁调用,必须使用
ReentrantLock来替换synchronized写法。
- 错误的
synchronized(lockObj) {
frequentIO();
}
- 正确的
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
frequentIO();
} finally {
lock.unlock();
}
参考原文: