05、Java 20 新特性 - 结构化并发(第二个孵化器版本)

Java 当前的并发实现是非结构化的,这可能会使处理多个任务的错误处理和取消变得困难。当异步启动多个任务时,如果第一个任务返回错误,我们目前无法取消剩余的任务。

通过JEP 中的示例代码来说明这一点:

Response handle() throws ExecutionException, InterruptedException {
        Future < String > user = executor.submit(() - > findUser());
        Future < Integer > order = executor.submit(() - > fetchOrder());
        String theUser = user.get(); // Join findUser    int theOrder = order.get();  // Join fetchOrder    return new Response(theUser, theOrder);}

user.get() 调用出错时,没有办法取消第二个任务,从而防止获取一个不会被使用的结果。

不过,如果将此代码重写为只使用一个线程,情况将变得简单得多:

Response handle() throws IOException {
        String theUser = findUser();
        int theOrder = fetchOrder();
        return new Response(theUser, theOrder);
        }

在Java 中,如果任务及其子任务之间的父子关系在语法上得到了表达,那么多线程编程将更加容易、可靠和可观察 - 就像单线程代码一样。语法结构将定义子任务的生命周期,并启用一个运行时表示线程间层次结构,从而实现错误传播和取消以及并发程序的有意义观察。

这就是所谓的“结构化并发”。现在使用新的 StructuredTaskScope API对代码示例进行重写:

Response handle() throws ExecutionException, InterruptedException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future < String > user = scope.fork(() - > findUser());
        Future < Integer > order = scope.fork(() - > fetchOrder());
        scope.join(); // Join both forks      scope.throwIfFailed();  // ... and propagate errors      // Here, both forks have succeeded, so compose their results      return new Response(user.resultNow(), order.resultNow());  }}

在结构化并发中,子任务代表任务工作。任务等待子任务的结果并监视它们的失败。StructuredTaskScope 类允许开发人员将任务结构化为并发子任务家族,并将它们作为一个单元协调。子任务通过单独分叉它们在自己的线程中执行,然后将它们作为一个单元加入并可能作为一个单元取消。父任务处理子任务的成功结果或异常。

与原始示例相比,这里涉及的线程的生命周期理解起来很容易。在所有情况下,它们的生命周期都限于词法作用域,即 try-with-resources 语句的主体。此外,使用 StructuredTaskScope 确保了许多有价值的属性:

  • 短路错误处理: 如果子任务失败,则另一个子任务将被取消(如果它尚未完成)。这由 ShutdownOnFailure 实现的取消策略管理;其他策略,如 ShutdownOnSuccess 也是可用的。
  • 取消传播: 如果在调用 join() 之前或期间中断运行 handle() 的线程,则当该线程退出作用域时,两个分叉将自动取消。
  • 清晰可见: 上面的代码具有明确的结构:设置子任务,等待它们完成或被取消,然后决定是成功(并处理子任务的结果,这些结果已经完成)还是失败(子任务已经完成,因此没有更多的清理工作)。

顺便说一下,结构化并发正好与虚拟线程同时出现在 Java 中,这绝非偶然。现代 Java 程序可能会使用大量的线程,并且需要正确而稳健地协调它们。结构化并发正好可以提供这种功能,同时还可以使观察工具按照开发人员的理解显示线程。

与 Java 19 有何不同

情况与Java 19 中的情况大致相同(请参见 JEP 428)。唯一的变化是更新了 StructuredTaskScope,以使其支持在任务作用域中创建的线程继承作用域值。这简化了在线程之间共享不可变数据。再次注意,JEP 处于孵化器阶段,因此需要将--enable-preview --add-modules jdk.incubator.concurrent 添加到命令行中,以便能够使用该功能。

Panama 项目

Java 20 包含了两个源自 Panama 项目的功能:

  • 外部函数与内存 API
  • Vector API

Panama 项目旨在改善 JVM 与外部(非 Java)库之间的连接。