文章目录
前言
Java 的并发并发模型在 JDK 21 迎来了 “革命性升级”,核心是解决传统并发编程的两大痛点:线程资源昂贵导致的高并发瓶颈和多任务协作混乱导致的代码复杂。这两个问题,JDK 21 分别用 “虚拟线程” 和 “结构化并发” 给出了答案。
一、虚拟线程(Virtual Threads):让 “多线程” 不再昂贵
传统并发的痛点:线程是 “重量级资源”
传统 Java 线程(平台线程)直接对应操作系统线程,创建一个线程要占用 1MB 左右内存,且操作系统能同时运行的线程数量有限(通常几千个)。如果遇到高并发场景(比如 10 万用户同时访问接口),线程池会瞬间耗尽,导致请求排队甚至失败。
比如下面的代码,用线程池处理 10 万任务,会因线程不足变得很慢:
// 传统线程池(最多 1000 个平台线程)
ExecutorService executor = Executors.newFixedThreadPool(1000);
// 模拟 10 万次 I/O 操作(如数据库查询、网络请求)
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
// 模拟 I/O 等待(比如查数据库)
Thread.sleep(100); // 此时线程完全空闲,但仍占用资源
return null;
});
}
原因:10 万任务要排队等待 1000 个线程,大部分时间线程在 “空等” I/O 结果,却浪费着宝贵的系统资源。
虚拟线程的解决方案:轻量级线程,数量 “几乎无限”
虚拟线程是 JVM 管理的 “轻量级线程”,不直接绑定操作系统线程。一个平台线程可以 “承载” 成千上万个虚拟线程,且创建成本极低(内存仅几 KB)。
核心优势:当虚拟线程执行 I/O 操作(如 sleep、数据库查询)时,JVM 会 “暂时释放” 它占用的平台线程,让其他虚拟线程使用。等 I/O 完成后,再重新分配一个平台线程继续运行。
// 虚拟线程池(自动管理,数量无上限)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 同样处理 10 万次 I/O 操作
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
Thread.sleep(100); // 虚拟线程等待时,平台线程会被释放
return null;
});
}
效果:10 万任务几乎同时启动,无需排队,因为虚拟线程不占用宝贵的平台线程资源,I/O 等待时会 “让出” 资源给其他任务。
通俗理解
- 平台线程 = 一辆满载的卡车,跑起来费油(资源),一次只能拉少量货物(任务)。
- 虚拟线程 = 自行车队,轻便灵活, thousands of 辆自行车可以共享一条马路(平台线程),I/O 等待时还会主动下车让道。
二、结构化并发(Structured Concurrency):让多任务协作 “有条理”
传统并发的痛点:多任务 “各自为政”,容易出乱子
当一个任务需要拆分多个子任务并行执行时(比如接口需要同时调用 3 个微服务,再汇总结果),传统写法会有两个问题:
1.子任务失败了,其他子任务还在瞎跑,浪费资源;
2.需要手动处理异常和线程关闭,代码臃肿。
比如下面的代码,假设 fetchUser() 失败,fetchOrder() 和 fetchCart() 还会继续执行:
// 传统方式:三个子任务并行执行
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<User> userFuture = executor.submit(() -> fetchUser());
Future<Order> orderFuture = executor.submit(() -> fetchOrder());
Future<Cart> cartFuture = executor.submit(() -> fetchCart());
try {
User user = userFuture.get(); // 等待用户信息
Order order = orderFuture.get(); // 等待订单信息
Cart cart = cartFuture.get(); // 等待购物车信息
return new Result(user, order, cart);
} catch (Exception e) {
// 手动取消所有子任务(容易遗漏)
userFuture.cancel(true);
orderFuture.cancel(true);
cartFuture.cancel(true);
throw e;
}
结构化并发的解决方案:子任务 “荣辱与共”
结构化并发将多个子任务视为一个 “整体”,用 StructuredTaskScope 管理:
- 父任务等待所有子任务完成;
- 任何一个子任务失败,自动取消其他子任务;
- 无需手动关闭线程,自动释放资源。
案例:子任务失败自动取消其他任务
// 结构化并发:子任务绑定到一个“作用域”
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 启动三个子任务(fork = 分叉执行)
Future<User> userFuture = scope.fork(() -> fetchUser());
Future<Order> orderFuture = scope.fork(() -> fetchOrder());
Future<Cart> cartFuture = scope.fork(() -> fetchCart());
scope.join(); // 等待所有子任务完成
scope.throwIfFailed(); // 若有子任务失败,直接抛出异常
// 所有任务成功,汇总结果
return new Result(
userFuture.result(),
orderFuture.result(),
cartFuture.result()
);
}
效果:如果 fetchUser() 抛出异常,scope.join() 会发现失败,自动调用 shutdown() 取消 fetchOrder() 和 fetchCart(),避免资源浪费。
通俗理解
- 传统并发 = 三个朋友各自去买奶茶,其中一个人没买到,另外两个人还在傻傻排队。
- 结构化并发 = 三个人组队买奶茶,队长(父任务)说:“有人没买到,大家都别买了,赶紧回来!”
三、虚拟线程 + 结构化并发:强强联合
案例:高并发接口同时调用多个服务
// 处理用户请求的接口
public Result handleRequest() throws Exception {
// 1. 创建虚拟线程的结构化作用域
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 2. 用虚拟线程启动子任务(默认使用虚拟线程)
Future<User> userFuture = scope.fork(() -> fetchUser()); // 调用用户服务
Future<Order> orderFuture = scope.fork(() -> fetchOrder()); // 调用订单服务
Future<Cart> cartFuture = scope.fork(() -> fetchCart()); // 调用购物车服务
// 3. 等待所有子任务完成,失败则取消其他任务
scope.join();
scope.throwIfFailed();
// 4. 汇总结果
return new Result(
userFuture.result(),
orderFuture.result(),
cartFuture.result()
);
}
}
这个接口能轻松应对数万并发请求,因为:
- 虚拟线程让每个子任务的创建成本几乎可以忽略;
- 结构化并发确保任何子任务失败时,其他任务及时停止,不浪费资源。
总结
JDK 21 的并发模型升级,本质是让开发者 “少关心线程,多关注业务”:
- 虚拟线程:解决 “线程不够用” 的问题,让高并发场景不再需要复杂的线程池调优;
- 结构化并发:解决 “多任务协作乱” 的问题,让并发代码像同步代码一样直观。
对于日常开发,优先在 I/O 密集型场景(如 Web 接口、微服务调用)中使用这两个特性,能显著提升系统吞吐量并简化代码。

被折叠的 条评论
为什么被折叠?



