JEP 428: 结构化并发以简化Java多线程编程

263 阅读5分钟

JEP 428:结构化并发以简化Java多线程编程

JEP 428,结构化并发(孵化器),已经从JDK 19的建议状态晋升为 目标状态。在Project Loom的保护伞下,该JEP建议通过引入一个库来简化多线程编程,将在不同线程上运行的多个任务视为一个原子操作。因此,它将简化错误处理和取消,提高可靠性,并增强可观察性。这仍然是一个孵化中的API

这使得开发人员可以使用 "并发 "类来组织他们的并发代码。 **[StructuredTaskScope](https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.concurrent/jdk/incubator/concurrent/StructuredTaskScope.html)**类。它将把一系列的子任务当作一个单元。这些子任务将在它们自己的线程上通过分叉单独创建,但然后作为一个单元加入,并可能作为一个单元取消;它们的异常或成功的结果将被聚合并由父任务处理,让我们看一个例子:

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());
   }
}

上面的handle()方法代表一个服务器应用程序中的任务。它通过创建两个子任务来处理一个传入的请求。比如 [ExecutorService.submit()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html#submit(java.util.concurrent.Callable)), **[StructuredTaskScope.fork()](https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.concurrent/jdk/incubator/concurrent/StructuredTaskScope.html#fork(java.util.concurrent.Callable))**接受一个 [Callable](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Callable.html)并返回一个 [Future](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html).与之不同的是 [ExecutorService](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html)不同的是,返回的 Future没有通过 [Future.get()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html#get()).这个API运行在JEP 425,虚拟线程(预览版)之上,也是针对JDK 19的。

上面的例子使用了 **StructuredTaskScope**API,所以要在JDK 19上运行它们,开发者必须添加jdk.incubator.concurrent**模块,并启用预览功能以使用虚拟线程。

如以下命令所示,编译上述代码:

`javac --release 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java`

运行该程序也需要同样的标志:

`java --enable-preview --add-modules jdk.incubator.concurrent Main`;

然而,人们可以直接使用源代码启动器来运行这个程序。在这种情况下,命令行将是:

`java --source 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java`

jshell选项也是可用的,但也需要启用预览功能:

jshell --enable-preview --add-modules jdk.incubator.concurrent

结构化并发带来的好处是很多的。它在调用者方法和其子任务之间建立了子母关系。例如,从上面的例子来看, **handle()**任务是一个父任务,而它的子任务**findUser()****fetchOrder()**,是子任务。因此,整个代码块变成了原子性的。它通过在线程转储中展示任务的层次结构来确保可观察性。它还能在错误处理中实现短路。如果其中一个子任务失败了,其他任务如果没有完成也会被取消。如果父级任务的线程在调用 "D "之前或期间被打断,那么两个分叉将被自动取消。 **join()**的调用之前或期间,两个分叉将在范围退出时自动取消。这些为并发代码的结构带来了清晰度,开发者现在可以像在单线程环境中运行一样推理和遵循代码,就像他们读完了一样。

在编程的早期,程序的流程是由普遍使用的 **GOTO**语句来控制程序的流程,这导致了混乱的、难以阅读和调试的面条状代码。随着编程范式的成熟,编程界了解到 **GOTO**语句是邪恶的。1969年,因《计算机编程的艺术》一书而广为人知的计算机科学家唐纳德-克努斯(Donald Knuth程序的有效编写辩护,他认为程序可以在没有 **GOTO**.后来,结构化编程的出现,解决了所有这些缺点。请看下面的例子:

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

上面的代码是结构化代码的一个例子。在单线程环境下,当方法被调用时,它是按顺序执行的。 **handle()**方法被调用。该 **fetchOrder()**方法不会在 **findUser()**方法开始。如果该 **findUser()**方法失败,下面的方法调用就根本不会启动,而 **handle()**方法隐含地失败了,这反过来又保证了原子操作要么成功要么不成功。它给了我们一个父子关系,在 **handle()**方法和它的子方法调用之间的父子关系,它遵循错误传播,并在运行时给我们一个调用栈。

然而,这种方法和推理并不适合我们目前的线程编程模型。例如,如果我们想写上面的代码与 **ExecutorService**,代码就会变成如下:

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

其中的子任务 **ExecutorService**中的子任务独立运行,所以它们可以独立地成功或失败。即使父任务被中断,中断也不会传播到子任务上,因此会产生一个泄露的情况。它失去了父级关系。这也使得调试变得困难,因为父任务和子任务出现在线程转储中不相关的线程的调用栈上。尽管代码看起来是有逻辑结构的,但它仍然停留在开发人员的头脑中,而不是在执行中;因此,并发代码变得非结构化。

观察到非结构化并发代码的所有这些问题,"结构化并发 "一词是由Martin Sústrik在他的博客文章中创造的,然后由Nathaniel J. Smith在他的结构化并发文章的笔记中推广。关于结构化并发,Oracle的技术顾问成员、Project Loom的项目负责人Ron PresslerInfoQ播客中说:

结构化的意思是,如果你催生了什么,你必须等待它并加入它。而这里的结构这个词与它在结构化编程中的用法相似。它的意思是,你的代码的块状结构反映了程序的运行时行为。因此,就像结构化编程为你提供的顺序控制流一样,结构化并发也为并发提供了同样的功能。

对深入了解结构化并发和学习背景故事感兴趣的开发者可以收听InfoQ播客、Ron Pressler的YouTube课程和Inside Java文章。