JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · CONCURRENCY · JEP 505

结构化并发:把子任务关进词法作用域

JDK 25 · 深钻主题 02 / 03 · 预计 55 分钟
深入四步深钻JEP 505需要 --enable-preview
一句话结构化并发不是更快的线程池,而是把一组子任务的生命周期、失败与取消收束到一个词法块里。JDK 25 仍是第五次预览,API 已换成 open() + Joiner,21 的 ShutdownOnFailure 不能直接编译。
本质:父任务不会在子任务仍运行时返回:fork 出去的任务,要么在 join 时给出结果,要么在 close 前被取消并终止。

学完你要能自己回答

  1. ExecutorService + Future 的非结构化并发,具体难在哪些地方?
  2. open()、Joiner、Subtask 各自负责什么?默认策略与常用 Joiner 的区别是什么?
  3. 失败、超时、所有者线程被中断时,取消如何传播?close() 为什么会等待?
  4. 为什么 21 的结构化并发代码在 25 编译失败?预览 API 的兼容边界是什么?
  5. 什么情况下不该用结构化并发,而应该继续用 ExecutorService?
01 非结构化并发:线程能创建,生命周期没人管02 结构化作用域:一棵有边界的任务树

非结构化并发:线程能创建,生命周期没人管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
结构化并发首先是一个生命周期与错误处理模型。它可能让请求更快,因为子任务并行;但「更快」不是它的定义,别把它当性能开关。
01 API 形状:open、Joiner、Subtask02 失败、超时与取消:短路,但不是强杀03 owner、fork 与 Subtask 的运行时规则

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();
}
Joinerjoin 返回失败 / 完成语义
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
  1. 默认 open():全部成功,join() 返回 null,isCancelled() 为 false。
  2. allSuccessfulOrThrow():一个失败,join 抛 FailedException,cause 是原始异常。
  3. anySuccessfulResultOrThrow():一个失败但另一个成功,join 返回成功结果。
  4. 同一 Joiner 下全部失败:join 抛 FailedException,cause 是其中一个失败。
  5. 超时:作用域取消,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 线程 forkWrongThreadException作用域绑定到打开它的线程
join 前 Subtask.get()IllegalStateException结果尚未确定
作用域逃逸 / 嵌套错误StructureViolationException运行时结构检查
块结束前未 joinclose 取消并等待可能抛异常,取决于实现路径
子任务不响应中断close 长时间阻塞取消是协作式的
不要把作用域存进字段、跨请求复用或交给别的线程 join。它的生命周期就是一个词法块;需要长期任务队列时用 ExecutorService。
01 模式:允许部分失败时,用 awaitAll 收结果02 常见坑

模式:允许部分失败时,用 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 逃避错误处理。
任何策略下,try-with-resources 的 close 都会取消未完成任务并等待终止。区别只在 join 抛不抛、返回什么。

常见坑02

  • 忘记 --enable-preview:编译和运行两端都要加,且必须是同一个 JDK 版本。
  • 照搬 21 的 API:ShutdownOnFailure / ShutdownOnSuccess / joinUntil 在 25 已移除。
  • 把 TimeoutException 当 FailedException 捕:超时会直接逃出 catch,close 仍会执行。
  • 吞掉中断:子任务不响应中断,close 会一直等到它结束;取消不是强杀。
  • 作用域逃逸:把 scope 存字段、跨请求复用或交给别的线程,会触发结构检查异常。
  • 共享可变状态:子任务并发执行,普通集合/计数器仍需要同步或并发容器。
  • 在子任务里再 fork 同一个 scope:fork 只能由 owner 线程调用,子任务里会抛 WrongThreadException。
结构化并发适合「一个请求/任务需要并发调用多个独立依赖,并统一处理失败」;不适合长期任务队列、事件循环或需要跨请求复用线程的场景。
01 合上页面,先回答02 版本边界:21 与 25 不能直接互换

合上页面,先回答01

先猜再看默认 open() 的 join() 返回什么?
返回 null(R 是 Void)。它的语义是等待所有子任务成功;任一失败就取消其余并抛 FailedException。
先猜再看一个子任务失败后,其他子任务会继续跑完吗?
默认策略会取消其他子任务,但取消是中断,不是强杀;join/close 会等待它们真正终止。若用 awaitAll,则不因失败取消,等全部结束。
01. 结构化并发解决 ExecutorService 的哪些问题?
把子任务生命周期限制在词法作用域内;失败短路并取消兄弟任务;owner 中断/作用域关闭时传播取消;线程 dump 能展示任务树。它不解决线程创建成本,也不保证更快。
02. anySuccessfulResultOrThrow 全部失败时抛什么?
抛 FailedException,cause 是其中一个子任务的异常;只要有一个成功,就取消其余并返回该成功结果。
03. 超时抛什么异常?应该怎么捕?
抛 StructuredTaskScope.TimeoutException(unchecked),不是 FailedException。要单独 catch TimeoutException;只 catch FailedException 会让超时逃逸。
04. close() 为什么会阻塞?
close 取消并等待所有子任务终止。取消只发送中断;如果子任务吞掉中断或卡在不可中断操作上,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;升级时必须重写并重新编译,不能只改启动参数。

不要把结构化并发写进公共 API 的签名。预览特性可能在下一个版本再次改签名;把它隔离在实现层,升级时只改一处。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。