Java 21正式引入了结构化并发(Structured Concurrency,预览)和Scoped Values(Incubator)。如果你觉得它们只是"又一个并发工具",可能错过了Java在这十年里最深刻的一次并发模型变革。这篇不讲API怎么用------讲它们解决了什么问题,以及为什么说"真正的杀手锏是结构化任务树"。
一、传统并发的"结构性缺失"
先看一段常见代码:
java
// 传统的"火抛"模式
Future<Order> orderFuture = es.submit(() -> fetchOrder(orderId));
Future<User> userFuture = es.submit(() -> fetchUser(userId));
// 两个任务各跑各的,互不关心
Order order = orderFuture.get(); // 可能等在这里
User user = userFuture.get(); // 也可能等在这里
这段代码有三个问题,任何一个问题都可能导致另两个无法被取消:
- 没有生命周期关联------如果fetchOrder抛异常,fetchUser还在后台跑,线程资源泄漏
- 没有"整体"的取消------你不能说"取消这整个查询请求",只能一个个cancel
- 任务树不透明------在jstack/thread dump里,你看不出fetchUser是属于哪个调用链的
java
// 问题1:子任务失控
try {
Order order = orderFuture.get(5, TimeUnit.SECONDS);
// 执行到这里时,fetchUser可能还在后台运行
// 如果fetchOrder超时,fetchUser在"幽灵"执行
} catch (TimeoutException e) {
userFuture.cancel(true); // 你得手动取消兄弟任务
throw e;
}
这就是"结构性缺失"------没有语言层面的机制保证子任务的生命周期和父任务一致。
二、StructuredTaskScope:任务有了"作用域"
java
// JDK 21的结构化并发
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Order> orderFuture = scope.fork(() -> fetchOrder(orderId));
Future<User> userFuture = scope.fork(() -> fetchUser(userId));
scope.join(); // 等待所有子任务完成
scope.throwIfFailed(); // 任何一个失败就抛出
return new Response(
orderFuture.resultNow(), // 非阻塞获取(已确保完成)
userFuture.resultNow()
);
}
// 离开try块时,所有子任务保证已结束
发生了什么?
- scope.fork()创建的子任务生命周期绑定到scope
- scope.join()等待所有子任务完成(或任何一个失败/取消)
- 离开try块时,scope的close()会自动处理未完成的子任务------取消它们
- ShutdownOnFailure策略:任何一个子任务失败,立即关闭scope,取消其他所有子任务
这就是"结构化"的含义:并发任务的生命周期嵌套在代码块的语法结构中。
2.1 取消策略对比
| 策略 | 行为 | 适用场景 |
|---|---|---|
| ShutdownOnFailure | 任何一个子任务失败,取消所有其他 | 所有调用缺一不可(如微服务编排) |
| ShutdownOnSuccess | 任何一个成功,取消所有其他 | 只要有一个结果就够(如多路由查询) |
| 自定义策略 | 继承StructuredTaskScope | 复杂的业务编排 |
2.2 自定义策略示例
java
// 自定义:前两个完成就算成功
public class QuorumScope<T> extends StructuredTaskScope<T> {
private final int quorum;
private final List<T> results = new CopyOnWriteArrayList<>();
private volatile boolean completed = false;
public QuorumScope(int quorum) {
this.quorum = quorum;
}
@Override
protected void handleComplete(Future<T> future) {
// 任务完成时的回调(无论成功/失败)
if (!completed && future.state() == Future.State.SUCCESS) {
results.add(future.resultNow());
if (results.size() >= quorum) {
completed = true;
shutdown(); // 达到quorum后关闭scope
}
}
}
public List<T> results() {
return List.copyOf(results);
}
}
三、ScopedValues:ThreadLocal的"正确"替代
3.1 ThreadLocal的问题
java
// 传统方式
private static final ThreadLocal<RequestContext> CTX = new ThreadLocal<>();
// 在某个方法中
CTX.set(new RequestContext(userId, traceId));
// 但在虚拟线程场景下
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// ❌ ThreadLocal的值是null!
// 虚拟线程不继承父线程的ThreadLocal
var ctx = CTX.get();
});
}
虚拟线程池和平台线程池不一样------虚拟线程不绑定平台线程,ThreadLocal的值不会自动传递。
就算不考虑虚拟线程,ThreadLocal也有两个固有问题:
- 可变性------任何地方的代码都能修改ThreadLocal,bug极难排查
- 内存泄漏------Web容器线程池场景下,ThreadLocal里的对象在线程归还池后残留
3.2 ScopedValues的方案
java
// JDK 21
private static final ScopedValue<RequestContext> CTX = ScopedValue.newInstance();
public void handle(Request request) {
var ctx = new RequestContext(request.getUserId(), generateTraceId());
ScopedValue.where(CTX, ctx)
.run(() -> {
// 这里以及所有被调用的方法,都可以读CTX
process(request);
});
// 离开run后,CTX自动还原为之前的值(或null)
}
private void process(Request request) {
// 读取,不可变
var ctx = CTX.get();
ctx.userId(); // ✅ 读
// ctx.userId = "xxx"; // ❌ 编译错误(RequestContext设计为不可变)
}
3.3 结构化继承
ScopedValue和StructuredTaskScope配合时,才是最强的
java
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
public Response handle(Request req) {
return ScopedValue.where(TRACE_ID, req.getTraceId())
.call(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Order> orderFuture = scope.fork(() -> fetchOrder());
Future<User> userFuture = scope.fork(() -> fetchUser());
scope.join();
scope.throwIfFailed();
return new Response(orderFuture.resultNow(),
userFuture.resultNow());
}
});
}
private Order fetchOrder() {
// ✅ 子任务自动继承ScopedValue
// 不需要手动传递,也不怕被修改
var traceId = TRACE_ID.get();
return orderService.get(traceId);
}
关键语义:
- ScopedValue是不可变的------一旦绑定,作用域内不能修改
- 子任务自动继承父任务的ScopedValue绑定
- 离开作用域后自动恢复之前的值
- 没有内存泄漏------不需要调remove()
四、Three Musketeers:虚拟线程 + 结构化并发 + ScopedValues
这三者放在一起,才是JDK 21并发模型的完整图景:
虚拟线程 → 解决"并发数不够"(千级→百万级)
结构化并发 → 解决"生命周期管理"(子任务随父任务一起结束)
ScopedValues → 解决"上下文传递"(不可变的请求级上下文,自动继承)
一个完整的例子:
java
public class OrderService {
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
public CompletableFuture<OrderResult> processOrder(OrderRequest request) {
return CompletableFuture.supplyAsync(() -> {
return ScopedValue.where(TRACE_ID, generateTraceId())
.call(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 这三个任务在虚拟线程上并发执行
var payment = scope.fork(() -> processPayment(request));
var inventory = scope.fork(() -> checkInventory(request));
var riskCtrl = scope.fork(() -> evaluateRisk(request));
scope.join(); // 等所有
scope.throwIfFailed(); // 任一失败则全部取消
return buildResult(payment.resultNow(),
inventory.resultNow(),
riskCtrl.resultNow());
}
});
});
}
}
五、迁移建议
| 当前做法 | 迁移方向 | 优先级 |
|---|---|---|
| ThreadLocal传递请求上下文 | ScopedValues | 高(特别是配合虚拟线程时) |
| CompletableFuture多个thenCombine火抛 | StructuredTaskScope | 中(逻辑清晰度提升显著) |
| 手动管理线程池+Future.cancel | StructuredTaskScope | 高(取消语义更难) |
| 继承ThreadPool做父子任务追踪 | ScopedValues + StructuredTaskScope | 低(已有生态可能更稳) |
六、总结
- StructuredTaskScope解决的是"子任务泄漏"------代码块结束,所有子任务结束
- ScopedValues解决的是"上下文泄漏和修改"------不可变、自动继承、无需清理
- 这两者的共同特征是结构化------在代码语法层面保证了并发任务的生命周期与作用域一致
- 它们和虚拟线程是互补的------虚拟线程解决数量,结构化解决管理,ScopedValues解决传递
- JDK 21中StructuredTaskScope还是预览(--enable-preview),ScopedValues在Incubator