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

Structured Concurrency:第五次预览的新 API

JDK 25 · 速查主题 02 / 06 · 预计 35 分钟
进阶35 分钟JEP 505需要 --enable-preview
一句话JDK 25 的结构化并发仍是预览,但 API 已换成 open() + Joiner:作用域是接口,子任务默认跑在虚拟线程上,失败或超时会取消兄弟任务。21 的 ShutdownOnFailure / ShutdownOnSuccess 在 25 已不存在。

学完你要能自己回答

  1. 结构化并发与 ExecutorService + Future 的本质差异是什么?
  2. open()、Joiner、Subtask 分别负责什么?默认策略与 allSuccessfulOrThrow 有何不同?
  3. 失败、超时、所有者线程被中断时,取消如何传播?close() 为什么可能等待?

状态:第五次预览,API 与 21 不兼容01

结构化并发从 JDK 21 开始预览,JDK 25 是第五次预览(JEP 505)。这一版把 StructuredTaskScope 改成接口,用静态 open(...) 创建,用 Joiner 表达完成策略;21 的 ShutdownOnFailure / ShutdownOnSuccess 已被移除。

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
不要把 21 的结构化并发示例直接搬到 25。即使都叫预览,API 形状已经变了;升级时必须重写并重新编译。

默认策略:全成功,或首个失败02

无参 open() 使用默认策略:等待所有子任务成功;任一子任务失败就取消其余任务,join() 抛 FailedException,原始异常在 cause 中。全部成功时 join() 返回 null(R 是 Void)。

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 默认在虚拟线程上执行子任务;也可以传自定义 ThreadFactory。
  • join() 只能由打开作用域的所有者线程调用;它等待所有子任务结束或被取消。
  • Subtask.get() 只能在 join 之后调用;提前调用抛 IllegalStateException。
  • try-with-resources 的 close() 会取消未完成任务并等待它们终止。
默认策略适合「所有结果都必需」的 fan-out/fan-in。只要有一个结果可选,就应该换 Joiner,而不是在默认策略里吞异常。

Joiner:把完成策略变成对象03

Joiner 负责在子任务完成时决定是否取消作用域,并产出 join 的结果。常用工厂:allSuccessfulOrThrow()、anySuccessfulResultOrThrow()、awaitAll()、awaitAllSuccessfulOrThrow()、allUntil(predicate)。

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.<String>allSuccessfulOrThrow())) {
    scope.fork(() -> "a");
    scope.fork(() -> "b");
    scope.join().forEach(s -> System.out.println(s.get()));
}
a
b

注意:Subtask 流的顺序不是 API 契约;本机这次恰好输出 a、b,换成有真实耗时的任务后顺序可能变化。需要稳定顺序时,按结果字段排序或改用 Map 关联。

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
    scope.fork(() -> { Thread.sleep(300); return "slow"; });
    scope.fork(() -> { Thread.sleep(100); return "fast"; });
    System.out.println("winner=" + scope.join());
}
winner=fast
Joinerjoin 返回失败时
open() 默认Void / null任一失败即取消其余,抛 FailedException
allSuccessfulOrThrow()Stream<Subtask<T>>任一失败即取消其余,抛 FailedException
anySuccessfulResultOrThrow()T任一成功即取消其余;全部失败抛 FailedException
awaitAll()Void不因失败取消,等待全部结束
awaitAllSuccessfulOrThrow()Void等待全部;有失败则抛 FailedException
anySuccessfulResultOrThrow 适合多副本、hedged request。它只保证「最先成功」,不保证副作用只发生一次;写操作要幂等。

失败、超时与关闭04

失败传播是短路的:一个子任务抛异常,作用域取消其余子任务(通过中断),join() 抛 FailedException。取消是协作式的,子任务必须响应中断;close() 会一直等到所有子任务真正终止。

try (var scope = StructuredTaskScope.open()) {
    var slow = scope.fork(() -> { Thread.sleep(5000); return "slow"; });
    scope.fork(() -> { throw new IllegalStateException("boom"); });
    try {
        scope.join();
    } catch (StructuredTaskScope.FailedException e) {
        System.out.println("failed cause=" + e.getCause());
    }
    System.out.println("slow state=" + slow.state());
}
failed cause=java.lang.IllegalStateException: boom
slow state=UNAVAILABLE
try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.<String>allSuccessfulOrThrow(),
        cfg -> cfg.withTimeout(java.time.Duration.ofMillis(100)))) {
    scope.fork(() -> { Thread.sleep(1000); return "late"; });
    try {
        scope.join();
    } catch (StructuredTaskScope.TimeoutException e) {
        System.out.println("timeout=" + e.getClass().getSimpleName());
    }
}
timeout=TimeoutException
  • 超时抛的是 StructuredTaskScope.TimeoutException(unchecked),不是 FailedException。
  • 所有者线程被中断时,join() 抛 InterruptedException;close 会取消并等待子任务。
  • 子任务吞掉中断或不响应中断,会让 close 等待很久,结构化并发也无法强制杀死线程。
  • 作用域逃逸、嵌套不当或未在词法范围内关闭,可能抛 StructureViolationException。
取消不是「立刻停止」。它只是中断;阻塞在不可中断操作上的任务仍会拖住 close。设计子任务时要把可中断性当成契约的一部分。

运行时规则:owner、fork 与 Subtask05

结构化并发在运行时强制「结构」:只有打开作用域的 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。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
结构化并发21–25 预览JEP 505 为第五次预览;需 --enable-preview。
`ShutdownOnFailure / ShutdownOnSuccess`25 已移除改用 open() + Joiner。
`open() / Joiner / Configuration`25 新增API 形状与 21 不兼容。
`joinUntil(Instant)`25 已移除改用 Configuration.withTimeout(Duration)。
默认 fork 线程虚拟线程可通过 ThreadFactory 定制。
ScopedValue 继承25 正式语义fork 继承绑定;手工线程不继承。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. 结构化并发最重要的不变量是什么?
作用域关闭时,所有子任务都已终止;父任务不会在子任务仍运行时返回或抛出。子任务的生命周期被限制在词法块内。
02. JDK 25 的 open() 默认策略是什么?join 返回什么?
默认等待所有子任务成功;任一失败就取消其余并抛 FailedException。全部成功时 join 返回 null(R 为 Void)。
03. anySuccessfulResultOrThrow 与 allSuccessfulOrThrow 的区别?
前者在任一子任务成功时立即取消其余并返回该结果,全部失败才抛 FailedException;后者等待全部成功,返回 Subtask 流,任一失败就取消其余并抛 FailedException。
04. 为什么 close() 可能长时间阻塞?
close 会取消并等待所有子任务终止。取消只是中断;如果子任务吞掉中断或阻塞在不可中断操作上,close 只能等它自然结束。
学习进度 记录保存在本机浏览器 · 不上传

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

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