1. 概述

在这个教程中,我们将探讨Java 19引入的孵化特性——结构化并发(JEP 428),它为多线程代码提供了更高效的并发能力管理。我们将指导您如何使用新API来管理多线程代码,提升其可维护性、可靠性和可观测性。

2. 主题

通过采用一种减少取消和关闭时常见问题(如线程泄露和取消延迟)的并发编程风格,增强多线程代码的健壮性。让我们先来看看一个未结构化的并发问题示例:

Future<Shelter> shelter;
Future<List<Dog>> dogs;
try (ExecutorService executorService = Executors.newFixedThreadPool(3)) {
    shelter = executorService.submit(this::getShelter);
    dogs = executorService.submit(this::getDogs);
    Shelter theShelter = shelter.get();   // Join the shelter
    List<Dog> theDogs = dogs.get();  // Join the dogs
    Response response = new Response(theShelter, theDogs);
} catch (ExecutionException | InterruptedException e) {
    throw new RuntimeException(e);
}

getShelter()运行时,如果getDogs()可能失败,代码可能不会察觉到,并会由于阻塞的shelter.get()调用而继续执行,导致不必要的问题。因此,只有在getShelter()完成并返回后,dogs.get()才会抛出异常,导致代码失败:

但这并不是唯一的问题。当执行代码的线程被中断时,它不会将中断传递给子任务。此外,如果第一个执行的子任务shelter抛出异常,它不会传递给狗狗的子任务,而是继续运行,浪费资源。

结构化并发试图解决这些问题,我们将在下一章中详细介绍。

3. 示例

对于结构化并发的示例,我们将使用以下记录:

record Shelter(String name) { }

record Dog(String name) { }

record Response(Shelter shelter, List<Dog> dogs) { }

我们将提供两个方法:获取一个Shelter

private Shelter getShelter() {
    return new Shelter("Shelter");
}

另一个是获取一个Dog列表:

private List<Dog> getDogs() {
    return List.of(new Dog("Buddy"), new Dog("Simba"));
}

由于结构化并发是孵化特性,我们需要使用以下参数运行应用程序:

--enable-preview --add-modules jdk.incubator.foreign

否则,我们可以添加一个module-info.java文件,并标记包为必需的。

让我们看一个例子:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<Shelter> shelter = scope.fork(this::getShelter);
    Future<List<Dog>> dogs = scope.fork(this::getDogs);
    scope.join();
    Response response = new Response(shelter.resultNow(), dogs.resultNow());
    // ...
}

由于StructuredTaskScope实现了AutoCloseable接口,我们可以在try-with-resources语句中使用它。StructuredTaskScope提供了两个子类,各有不同的用途。在这篇教程中,我们将使用ShutdownOnFailure(),它在遇到错误时关闭子任务。

还有ShutdownOnSuccess()构造函数,它的作用相反,在成功时关闭子任务。这种短路模式有助于避免不必要的工作。

使用StructuredTaskScope的方式类似于同步代码的结构。创建scope的线程是所有者,scope允许我们在其中fork更多的子任务。这些代码异步执行,通过join()方法可以阻塞直到所有任务完成。

每个任务都可以使用scopeshutdown()方法终止其他任务。throwIfFailed()方法提供了另一种选择:

scope.throwIfFailed(e -> new RuntimeException("ERROR_MESSAGE"));

它允许在任何子任务失败时传播异常。此外,我们还可以设置截止日期,使用joinUntil

scope.joinUntil(Instant.now().plusSeconds(1));

如果任务在截止日期前还未完成,这将会抛出异常。

4. 总结

本文讨论了无结构并发的缺点以及结构化并发如何解决这些问题。我们学习了如何处理错误和实现截止日期。同时,我们也看到新的构造方式使编写可维护、易读且可靠的多线程同步代码变得更加容易。

一如既往,这些示例也可以在GitHub上找到。