写点什么

JEP 533 加强 JDK 27 中 Java 结构化并发的异常处理

作者:A N M Bazlur Rahman
  • 2026-05-15
    北京
  • 本文字数:1976 字

    阅读完需:约 6 分钟

JEP 533(结构化并发第七个预览版)已经升级为 JDK 27 的正式功能。以 JDK 19 首次孵化以及 JDK 21 开始的多轮预览为基础,本轮迭代继续对该 API 进行优化。本轮更新的重点主要集中在如何将异常从作用域中传播出来。

结构化并发通过 java.util.concurrent.StructuredTaskScope 类提供,结合 Joiner 抽象,可以将一组相关的子任务视为单个工作单元。它解决了临时线程管理无法解决的三个问题:将子任务的生命周期限制在父作用域内、可靠地传播取消操作,以及在可观测性工具中呈现线程层次结构。预览版 6(JDK 26)新增了 onTimeout() 回调,并将 allSuccessfulOrThrow() 调整为返回 List。预览版 7 延续了这一方向,并重点关注异常处理的易用性和类型安全性。JEP 533 将本轮迭代定义为一次重点优化:StructuredTaskScope 和 Joiner 接口新增了第三个类型参数“用于指定 StructuredTaskScope 类中 join() 方法可能抛出的异常类型”,并新增了一个静态 open 方法,用于“实现默认的 join 策略,并使用给定的 UnaryOperator 生成 StructuredTaskScope 配置”。

最显著的变化是三个标准连接器(joiner)的 join() 方法所抛出的新异常类型。在最近的预览版中,当子任务失败时,Joiner.allSuccessfulOrThrow()anySuccessfulOrThrow()awaitAllSuccessfulOrThrow() 会抛出预览版特有的 FailedException 异常。现在,在预览版 7 中,这些连接器会抛出 ExecutionException,这与 Future.get() 中长期用于指示子任务失败的封装器相同。异常原因信息会保留在 getCause() 中,因此可以直接沿用熟悉的 catch-then-switch 模式:

try (var scope = StructuredTaskScope.open()) {    Subtask<String> user   = scope.fork(() -> findUser(userId));    Subtask<List<Order>> o = scope.fork(() -> fetchOrders(userId));    scope.join();    return new Response(user.get(), o.get());} catch (ExecutionException e) {    switch (e.getCause()) {        case IOException ioe       -> handleIo(ioe);        case TimeoutException te   -> handleTimeout(te);        default                    -> throw e;    }}
复制代码

这次变更缩小了经典并发代码与结构化作用域之间的概念差异。已经在早期预览版中捕获 FailedException 异常的团队,在迁移至 JDK 27 时,只需要将这些捕获语句更新为捕获 ExecutionException 异常。

第二个改动是结构上的。现在,StructuredTaskScope 和 Joiner 接口新增了第三个类型参数 R_X,用于表示 join() 可能抛出的异常类型。之前的签名是 Joiner,现在变成了 Joiner。名称 R_X 遵循该 JEP 的约定,用于在文档中将异常类型与结果类型及 join() 返回类型区分开来;编译器会像对待任何其他类型参数一样对待它。如果应用程序代码通过 open() 使用了所提供的 joiner ,编译器就会推导出所有内容,源代码看起来与之前相同。对于编写自定义连接器的库作者而言,throws 子句已经成为类型的一部分,而非由实现单独声明的内容。这样一来,签名就变得更加真实可信了,并且为调用者提供了一个关于 join() 方法的精确的异常检查契约。

第三项更改是新增了一个 open 重载,它将默认的合并策略(即无参数 open() 的行为,它会等待所有子任务成功或任一子任务失败)与一个配置操作符(使用 UnaryOperator 设置作用域 的 Configuration )配对:

try (var scope = StructuredTaskScope.open(        cfg -> cfg.withTimeout(Duration.ofSeconds(2)).withName("checkout"))) {    scope.fork(() -> fetchCart(userId));    scope.fork(() -> fetchProfile(userId));    scope.join();}
复制代码

之前,如果要在默认的快速失败策略中应用超时、名称或自定义线程工厂,就必须在操作符中同时传入一个 Joiner。新的工厂方法消除了这一繁琐步骤。该重载接受一个 UnaryOperator 参数,这与预览版 6 中引入的更严格的类型检查一致。

结构性保障保持不变:子任务会继承 ScopedValue 绑定(JEP 506),JSON 线程转储格式仍然会向工具暴露作用域层次结构,而且,当作用域在 try-with-resources 之外使用,或从非所有者线程分叉时,仍然会抛出 StructureViolationException 异常。

预览版 7 并非重新设计。预览版 5 中确定的 API 框架保持不变,本轮更改仅限于易用性和类型方面,而非结构。对于关注该 API 的团队而言,每次预览范围的缩小,都表明设计正在趋于稳定。结构化并发已经经历了两个孵化期和数个预览版。尽管 JEP 533 并未明确最终时间表,但该 API 似乎正在趋于稳定。

要对该提案进行测试,开发人员可以在 JDK 27 早期访问版本中使用 --enable-preview 启用该预览功能。在 API 最终确定之前,通过 OpenJDK 邮件列表收集的反馈意见将继续被用于完善该 API。

原文链接:https://www.infoq.com/news/2026/05/jep-533-jdk-27/