InfoQ

JEP 543 Finalizes Structured Concurrency in JDK 28, Ending Seven Preview Rounds

6.9内容质量
JEP 543 Finalizes Structured Concurrency in JDK 28, Ending Seven Preview Rounds

TL;DR · AI 摘要

JEP 543 Finalizes Structured Concurrency in JDK 28, Ending Seven Preview Rounds - InfoQ InfoQ Homepage News JEP 543 Fina...

核心要点

  • 主题聚焦:JEP 543 Finalizes Structured Concurrency in JDK
  • 来源:InfoQ,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#后端#安全
打开原文

JEP 543 在 JDK 28 中最终确定结构化并发,结束七轮预览 - InfoQ

InfoQ 首页 News JEP 543 在 JDK 28 中最终确定结构化并发,结束七轮预览

Java

从 Token 到功能:为 AI 辅助工程构建成本归属模型(10 月 29 日网络研讨会)

JEP 543 在 JDK 28 中最终确定结构化并发,结束七轮预览

2026 年 10 月 19 日 4 分钟阅读

作者

  • A N M Bazlur Rahman

#### 关注我们

YouTube

232K 粉丝

LinkedIn

26K 粉丝

Instagram

新

RSS

19K 读者

X

57.1k 粉丝

Facebook

21K 喜欢

Bluesky

收听本文 -

0:00

音频准备就绪

您的浏览器不支持音频元素。

正常

1.25x

1.5x

喜欢

新下拉阅读列表

  • 阅读列表

JEP 543(结构化并发)已整合到 JDK 28 中,使 java.util.concurrent 中的 StructuredTaskScope API 达到最终状态,与 JDK 27 的第七次预览相比没有 API 变更。对于已使用 JDK 27 的代码库,迁移只需重新编译时移除 --enable-preview 参数。

最终确定标志着 Project Loom 功能中最长的预览周期结束。结构化并发最初作为孵化器 API 在 JEP 428(JDK 19)和 JEP 437(JDK 20)中亮相,随后经历了七次预览:JEP 453(JDK 21)、JEP 462(JDK 22)、JEP 480(JDK 23)、JEP 499(JDK 24)、JEP 505(JDK 25)、JEP 525(JDK 26)和 JEP 533(JDK 27)。在每次迭代中,核心目标都是将不同线程中运行的相关子任务组视为一个工作单元,确保一个子任务的失败会取消其他子任务,且父级作用域在所有子线程终止前不能退出。传统的 ExecutorService 和 Future 管道不会强制这些边界:父级方法可以在子任务在后台继续运行时返回,而子任务失败仅在调用者最终调用 Future.get() 时才会显现。

该 API 的核心是 StructuredTaskScope<T, R, R_X extends Throwable>,这是一个通过静态工厂方法而非公共构造函数公开的密封接口。零参数 open() 工厂返回的范围其 join() 方法会等待所有子任务成功,第一个失败时取消其余任务,并抛出 ExecutionException:

code
Response handle() throws ExecutionException, InterruptedException {
    try (var scope = StructuredTaskScope.open()) {
        Subtask<String>  user  = scope.fork(() -> findUser());
        Subtask<Integer> order = scope.fork(() -> fetchOrder());
        scope.join(); // 如果任一子任务失败,会抛出 ExecutionException
        return new Response(user.get(), order.get());
    }
}

每个 fork() 调用默认会启动一个虚拟线程;join() 是所有者线程等待结果的唯一入口点;try-with-resources 块结束时的 close() 会取消任何未完成的子任务并等待其线程终止。完成策略通过 Joiner<T, R, R_X> 接口定义:Joiner.awaitAllSuccessfulOrThrow() 匹配上述默认策略,Joiner.allSuccessfulOrThrow() 对于同构子任务会直接从 join() 返回 List<T>,Joiner.anySuccessfulOrThrow() 返回第一个成功结果并取消其余任务,Joiner.allUntil(Predicate) 则在谓词匹配时取消作用域。

最终确定的 API 保留了在 JDK 27 中确立的确切结构。JEP 533 引入了第三个类型参数 R_X,使得 join() 可以声明其 Joiner 能抛出的具体异常类型,而不是使用专用于预览功能的 FailedException。在内置的抛异常 Joiner 中,join() 抛出 ExecutionException,这与 Future.get() 长期使用的包装异常一致。来自 JDK 26 的团队需要做出更多更改。他们将看到 FailedException 被 ExecutionException 取代,并且 R_X 会被添加到自定义 Joiner 的签名中。此外,StructuredTaskScope.TimeoutException 已被移除,转而通过 CancelledByTimeoutException 作为原因来报告超时,Joiner.awaitAll() 不再可用,onTimeout() 回调已被抽象的 timeout() 方法取代。

自定义策略是第三个类型参数存在的意义所在。例如,一个等待多数副本读取的 joiner 可以抛出其自身的受检异常:

code
class QuorumJoiner<T> implements Joiner<T, List<T>, QuorumException> {

    private final int quorum;
    private final Queue<T> results = new ConcurrentLinkedQueue<>();

    QuorumJoiner(int quorum) {
        this.quorum = quorum;
    }

    @Override
    public boolean onComplete(Subtask<T> subtask) {
        if (subtask.state() == Subtask.State.SUCCESS) {
            results.add(subtask.get());
        }
        return results.size() >= quorum; // true 取消剩余读取
    }

    @Override
    public List<T> result() throws QuorumException {
        if (results.size() < quorum) {
            throw new QuorumException(results.size(), quorum);
        }
        return List.copyOf(results).subList(0, quorum);
    }

    @Override
    public List<T> timeout() throws QuorumException {
        throw new QuorumException(results.size(), quorum,
                new CancelledByTimeoutException());
    }
}

try (var scope = StructuredTaskScope.open(new QuorumJoiner<String>(2))) {
    replicas.forEach(r -> scope.fork(() -> r.read(key)));
    List<String> values = scope.join(); // 抛出 QuorumException
}

由于 joiner 将 QuorumException(一个应用定义的受检异常)声明为其 R_X,join() 会直接抛出它,而不是将其包装在 ExecutionException 中。timeout() 按照 JDK 27 的约定,通过 CancelledByTimeoutException 作为原因抛出相同的异常。

超时和配置语义也保持不变。将 UnaryOperator<Configuration> 传递给 open() 可以为可观测性配置作用域名称、自定义 ThreadFactory 或一个截止时间,如果在 join() 之前或期间过期,该截止时间会取消作用域:

code
try (var scope = StructuredTaskScope.open(
        cf -> cf.withTimeout(Duration.ofSeconds(2)).withName("profile-page"))) {
    Subtask<String> user = scope.fork(() -> findUser());
    Subtask<Integer> order = scope.fork(() -> fetchOrder());
    scope.join();
    return new Response(user.get(), order.get());
} catch (ExecutionException e) {
    return switch (e.getCause()) {
        case CancelledByTimeoutException _ -> handleTimeout();
        case IOException ioe -> handleIo(ioe);
        default -> throw e;
    };
}

API 对结构也有严格要求。只有打开作用域的线程可以调用 fork() 和 join(),且必须在 join() 返回后才能通过 Subtask.get() 读取结果。结构误用(如逆序关闭作用域)会立即抛出 StructureViolationException 异常,而不是导致线程泄漏。为了上下文传播和调试,子任务会继承作用域值绑定(JEP 506),并且在 JSON 线程转储中会显示在父任务之下。

一个细节被刻意保留未明确:当多个子任务同时失败时,API 不会说明 join() 会报告哪个失败。Viktor Klang 在代码审查期间提出了这个问题,Oracle 的 JEP 负责人 Alan Bateman 重写了 Javadoc,仅承诺异常来自某个失败的子任务。

对于使用 JDK 27 的团队,迁移到最终 API 只需构建配置变更,无需代码修改。最终版本的提交仅移除了预览注解,未改变任何方法签名,因此团队可以从 javac、测试运行器和启动脚本中删除 --enable-preview 参数,然后在包含该变更的 JDK 28 早期访问构建版本上重新编译。JDK 28 计划于 2027 年 3 月正式发布。

更大的变化影响库和框架维护者,他们现在可以在不强制用户启用预览标志的情况下构建结构化并发功能。velo/structured-concurrency 项目已将 JEP 543 API 回退到 JDK 25,并提供了升级到 JDK 类后的迁移方案。

关于作者

作者信息

#### A N M Bazlur Rahman

显示更多

显示更少

#### 本内容属于 Java 主题

##### 相关主题:

  • 开发
  • Java
  • Project Loom
  • JDK 28
  • JEP
  • 结构化并发
  • 相关编辑
  • 相关赞助商 《实用的智能体编码实践手册》
  • 相关赞助商 大规模自动化代码转换。修复漏洞、迁移框架、现代化所有仓库中的遗留代码。免费试用 Moderne

InfoQ 新闻letter

每周五发送 InfoQ 最新内容摘要。加入超过 25 万名高级开发者的社区。查看示例

我们保护您的隐私。