结构化并发:把子任务关进词法作用域
学完你要能自己回答
- ExecutorService + Future 的非结构化并发,具体难在哪些地方?
- open()、Joiner、Subtask 各自负责什么?默认策略与常用 Joiner 的区别是什么?
- 失败、超时、所有者线程被中断时,取消如何传播?close() 为什么会等待?
- 为什么 21 的结构化并发代码在 25 编译失败?预览 API 的兼容边界是什么?
- 什么情况下不该用结构化并发,而应该继续用 ExecutorService?
非结构化并发:线程能创建,生命周期没人管01
ExecutorService + Future 把任务提交和结果获取分开:提交者、执行者、等待者可以是三个互不相关的线程。这种灵活性是特性,也是负担——没有任何结构保证子任务会在父任务返回前结束。
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
Future<User> user = pool.submit(this::findUser);
Future<Order> order = pool.submit(this::fetchOrder);
try {
return new Response(user.get(), order.get());
} finally {
pool.shutdownNow(); // 取消、等待、异常处理都要自己补
}
- 生命周期:子任务可能比父任务活得久;线程泄漏不会自动暴露。
- 失败处理:一个 Future 失败,其他 Future 不会自动取消,需要手工协调。
- 取消:
cancel(true)只是中断;不响应中断的任务仍会继续跑。 - 可观测性:线程 dump 里看不到「这些线程属于哪个请求」的层级。
- 结果组合:
get()的阻塞、超时、异常包装都要逐个处理。
结构化作用域:一棵有边界的任务树02
StructuredTaskScope 把子任务组织成一棵以 owner 线程为根的树。fork 创建子任务,join 等待整棵树,close 取消并等待剩余任务。父任务的作用域不结束,子任务就不会被遗弃。
import java.util.concurrent.*;
public class BasicScope {
record User(String id) {}
record Order(String id) {}
static User loadUser() throws Exception {
Thread.sleep(100);
return new User("u-1");
}
static Order loadOrder() throws Exception {
Thread.sleep(150);
return new Order("o-1");
}
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(BasicScope::loadUser);
var order = scope.fork(BasicScope::loadOrder);
scope.join();
System.out.println(user.get() + " / " + order.get());
}
}
}
javac --release 25 --enable-preview BasicScope.java
java --enable-preview BasicScope
User[id=u-1] / Order[id=o-1]
| 性质 | 结构化并发如何保证 |
|---|---|
| 词法生命周期 | fork/join/close 都在 try-with-resources 块内 |
| 失败短路 | 默认策略下任一子任务失败,取消其余子任务 |
| 取消传播 | owner 被中断或作用域关闭时,取消所有未完成子任务 |
| 结果可见性 | join 返回后,Subtask 的状态才可用 |
| 可观测性 | 线程 dump 按作用域展示父子层级 |
| 异常聚合 | join 抛 FailedException,原始异常在 cause |
API 形状:open、Joiner、Subtask01
JDK 25 的 StructuredTaskScope 是接口,不能 new。用静态 open() 创建;无参 open() 使用默认策略:等待所有子任务成功,任一失败就取消其余。更复杂的完成策略由 Joiner 表达。
public sealed interface StructuredTaskScope<T, R>
extends AutoCloseable {
static <T> StructuredTaskScope<T, Void> open();
static <T, R> StructuredTaskScope<T, R> open(
Joiner<? super T, ? extends R> joiner);
static <T, R> StructuredTaskScope<T, R> open(
Joiner<? super T, ? extends R> joiner,
Function<Configuration, Configuration> config);
<U extends T> Subtask<U> fork(Callable<? extends U> task);
<U extends T> Subtask<U> fork(Runnable task);
R join() throws InterruptedException;
boolean isCancelled();
void close();
}
| Joiner | join 返回 | 失败 / 完成语义 |
|---|---|---|
open() 默认 | Void / null | 任一失败即取消其余,抛 FailedException |
allSuccessfulOrThrow() | Stream<Subtask<T>> | 任一失败即取消其余,抛 FailedException |
anySuccessfulResultOrThrow() | T | 任一成功即取消其余;全部失败抛 FailedException |
awaitAll() | Void | 不因失败取消,等待全部结束 |
awaitAllSuccessfulOrThrow() | Void | 等待全部;有失败则抛 FailedException |
allUntil(predicate) | Stream<Subtask<T>> | 由 predicate 决定何时完成 |
Configuration 提供 withTimeout(Duration)、withName(String)、withThreadFactory(ThreadFactory)。超时从 open 开始计时;到期后作用域被取消,join() 抛 TimeoutException。
public interface Configuration {
Configuration withThreadFactory(ThreadFactory factory);
Configuration withName(String name);
Configuration withTimeout(Duration timeout);
}
join() 声明 throws InterruptedException;FailedException 和 TimeoutException 都是 unchecked。不要把超时当成 FailedException 捕获,否则异常会直接逃出 try。失败、超时与取消:短路,但不是强杀02
默认策略下,任一子任务抛异常,作用域立即取消其余子任务,join() 抛 FailedException,原始异常在 cause 中。被取消的子任务状态是 UNAVAILABLE;这不是「没开始」,而是「没有可用结果」。
import java.util.concurrent.*;
import java.time.Duration;
public class ScopePolicies {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> 1);
scope.fork(() -> 2);
System.out.println("default join=" + scope.join());
System.out.println("cancelled=" + scope.isCancelled());
}
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>allSuccessfulOrThrow())) {
scope.fork(() -> "ok");
scope.fork(() -> { throw new IllegalStateException("boom2"); });
try {
scope.join();
} catch (StructuredTaskScope.FailedException e) {
System.out.println("allSuccessful failed=" + e.getCause());
}
}
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
scope.fork(() -> { throw new IllegalStateException("first-fail"); });
scope.fork(() -> { Thread.sleep(50); return "second-ok"; });
System.out.println("any=" + scope.join());
}
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
scope.fork(() -> { throw new IllegalStateException("a"); });
scope.fork(() -> { throw new IllegalArgumentException("b"); });
try {
scope.join();
} catch (StructuredTaskScope.FailedException e) {
System.out.println("any all failed=" + e.getCause());
}
}
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>allSuccessfulOrThrow(),
cfg -> cfg.withTimeout(Duration.ofMillis(80)))) {
scope.fork(() -> { Thread.sleep(1000); return "late"; });
try {
scope.join();
} catch (StructuredTaskScope.TimeoutException e) {
System.out.println("timeout=" + e.getClass().getSimpleName());
}
}
}
}
javac --release 25 --enable-preview ScopePolicies.java
java --enable-preview ScopePolicies
default join=null cancelled=false allSuccessful failed=java.lang.IllegalStateException: boom2 any=second-ok any all failed=java.lang.IllegalStateException: a timeout=TimeoutException
- 默认
open():全部成功,join()返回 null,isCancelled()为 false。 allSuccessfulOrThrow():一个失败,join 抛 FailedException,cause 是原始异常。anySuccessfulResultOrThrow():一个失败但另一个成功,join 返回成功结果。- 同一 Joiner 下全部失败:join 抛 FailedException,cause 是其中一个失败。
- 超时:作用域取消,join 抛 TimeoutException;它不是 FailedException 的子类。
取消通过中断传播。子任务如果正在 sleep、阻塞 I/O 或可中断的锁上,会收到 InterruptedException;如果它吞掉中断或阻塞在不可中断操作上,close() 只能等它自然结束。结构化并发保证的是「不遗弃」,不是「瞬间杀死」。
Subtask.state() 有 SUCCESS、FAILED、UNAVAILABLE 三态。get() 只能在 join 之后调用;join 前调用抛 IllegalStateException。owner、fork 与 Subtask 的运行时规则03
作用域绑定到打开它的 owner 线程。只有 owner 能 fork/join;Subtask 的结果只能在 join 之后读取;作用域不能逃逸出词法块,也不能交给别的线程复用。
import java.util.concurrent.*;
public class ScopeRules {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open()) {
var subtask = scope.fork(() -> 1);
try {
subtask.get();
} catch (Exception e) {
System.out.println("get before join=" + e.getClass().getSimpleName());
}
var nested = scope.fork(() -> {
try {
scope.fork(() -> 2);
return "forked";
} catch (Exception e) {
return e.getClass().getSimpleName();
}
});
scope.join();
System.out.println("subtask state=" + subtask.state());
System.out.println("fork from subtask=" + nested.get());
}
}
}
get before join=IllegalStateException subtask state=SUCCESS fork from subtask=WrongThreadException
| 操作 | 结果 | 说明 |
|---|---|---|
| 非 owner 线程 fork | WrongThreadException | 作用域绑定到打开它的线程 |
join 前 Subtask.get() | IllegalStateException | 结果尚未确定 |
| 作用域逃逸 / 嵌套错误 | StructureViolationException | 运行时结构检查 |
| 块结束前未 join | close 取消并等待 | 可能抛异常,取决于实现路径 |
| 子任务不响应中断 | close 长时间阻塞 | 取消是协作式的 |
模式:允许部分失败时,用 awaitAll 收结果01
默认策略和 allSuccessfulOrThrow() 都是 fail-fast:一个失败就取消其余。若业务允许「部分成功」,用 awaitAll() 等全部结束,再逐个检查状态。下面的输出来自本机验证。
import java.util.concurrent.*;
public class PartialResults {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>awaitAll())) {
var ok = scope.fork(() -> "ok");
var bad = scope.fork(() -> {
throw new IllegalStateException("bad");
});
scope.join();
System.out.println("ok=" + ok.state() + "/" + ok.get());
System.out.println("bad=" + bad.state() + "/" + bad.exception());
}
}
}
ok=SUCCESS/ok bad=FAILED/java.lang.IllegalStateException: bad
awaitAll()不因失败取消,适合批量导入、可选 enrichment、部分结果可用的场景。awaitAllSuccessfulOrThrow()也等待全部,但只要有一个失败就抛 FailedException。- 失败任务的结果不要调
get();先看state(),需要原因时用exception()。 - 部分结果模式要定义清楚:失败是跳过、降级,还是整体失败。不要用 awaitAll 逃避错误处理。
常见坑02
- 忘记 --enable-preview:编译和运行两端都要加,且必须是同一个 JDK 版本。
- 照搬 21 的 API:
ShutdownOnFailure/ShutdownOnSuccess/joinUntil在 25 已移除。 - 把 TimeoutException 当 FailedException 捕:超时会直接逃出 catch,close 仍会执行。
- 吞掉中断:子任务不响应中断,close 会一直等到它结束;取消不是强杀。
- 作用域逃逸:把 scope 存字段、跨请求复用或交给别的线程,会触发结构检查异常。
- 共享可变状态:子任务并发执行,普通集合/计数器仍需要同步或并发容器。
- 在子任务里再 fork 同一个 scope:fork 只能由 owner 线程调用,子任务里会抛 WrongThreadException。
合上页面,先回答01
先猜再看默认 open() 的 join() 返回什么?
先猜再看一个子任务失败后,其他子任务会继续跑完吗?
01. 结构化并发解决 ExecutorService 的哪些问题?
02. anySuccessfulResultOrThrow 全部失败时抛什么?
03. 超时抛什么异常?应该怎么捕?
04. close() 为什么会阻塞?
版本边界:21 与 25 不能直接互换02
import java.util.concurrent.*;
public class OldApi {
void fetch() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> "a");
scope.join().throwIfFailed();
}
}
}
javac --release 25 --enable-preview OldApi.java
OldApi.java:5: error: cannot find symbol
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
^
symbol: class ShutdownOnFailure
location: interface StructuredTaskScope
Note: OldApi.java uses preview features of Java SE 25.
Note: Recompile with -Xlint:preview for details.
1 error
| 维度 | JDK 21 预览 | JDK 25 预览 |
|---|---|---|
| 创建 | new StructuredTaskScope.ShutdownOnFailure() | StructuredTaskScope.open(...) |
| 类型 | 类 | 接口 |
| 策略 | ShutdownOnFailure / ShutdownOnSuccess 子类 | Joiner 工厂或自定义 Joiner |
| join 返回 | 作用域自身 / 策略 result() | 由 Joiner 的 R 决定 |
| 超时 | joinUntil(Instant) | Configuration.withTimeout(Duration) |
| 状态 | 预览,需 --enable-preview | 预览,需 --enable-preview |
两边都叫「预览」,但 API 形状不兼容。21 的预览 class 是 65.65535,25 只识别 69.65535;升级时必须重写并重新编译,不能只改启动参数。