CompletableFuture入门

翻译自https://www.baeldung.com/java-CompletableFuture

1.介绍

本文是 CompletableFuture 类的功能和用例的指南, CompletableFuture 类是作为 Java 8 中对 Concurrency 包中API的改进而引入的。全文较长,关键点用黑色字体加粗表示。

需要具备的前提知识点:

  • Thread类、Runnable接口、 Future接口、Lambda表达式、函数式编程

2. Java中的异步计算

异步计算很难调试。 通常我们希望将任何复杂的计算拆开视为一系列步骤。 但是在异步计算的情况下,通过回调表示的动作往往分散在代码中,也可能相互嵌套在内部。 当我们需要处理其中一个步骤中可能发生的错误时,情况会变得更加糟糕。

Java 5中,引入了 Future 接口,以作为异步计算的结果,但是它没有任何方法可以组合这些计算结果或处理可能的错误。

Java 8中,引入了 CompletableFuture 类。 除 Future 接口外,它还实现了 CompletionStage 接口。 该接口定义了可以与其他步骤组合的异步计算步骤的协定。

同时, CompletableFuture 类是一个构件和框架,具有大约50种不同的方法,用于组成,组合,执行异步计算的步骤和处理过程中出现的错误。如此众多的API可能会让人不知所措,但这些API大多属于几种清晰明确的用例,下面会演示典型案例。

3.将 CompletableFuture 作为简单的 Future

首先, CompletableFuture 类实现了 Future 接口,因此你可以将其用作 Future 来实现,但需要额外的完成逻辑。

例如,你可以使用 CompletableFuture 的无参构造函数创建此类的实例,像 Future 一样表示将来的计算结果,将其返回给调用者,并在将来的某个时间使用 complete() 方法完成该过程得到结果。调用者可以使用 get() 方法来阻塞自己的当前线程,直到 get() 方法获取到计算结果为止。

在下面的示例中,我们有一个方法,该方法创建一个 CompletableFuture 实例,然后在另一个线程中分离一些计算并立即返回 Future
计算完成后,该方法通过将结果提供给 complete() 方法来完成 Future

public  Future calculateAsync() throws InterruptedException {
    CompletableFuture  completableFuture
      = new CompletableFuture <>();
 
    // 为了简化代码,使用了Java线程池的Executor来创建和执行线程
    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });
 
    return completableFuture ;
}

注意,calculateAsync() 方法返回一个 Future 接口的实例。
当我们准备好进入阻塞状态来获取计算结果时,我们只需调用定义好的calculateAsync() 方法,接收 Future 实例,并在 Future 实例上调用 get()方法,直到得到计算结果。

还要注意的是, get() 方法会抛出一些受检异常,即 ExecutionException(表示计算过程中发生的异常)和InterruptedException(表示执行方法的线程被中断的异常)。

Future  CompletableFuture = calculateAsync();

// ... do something else

String result = CompletableFuture.get();
assertEquals("Hello", result);

如果你已经知道计算结果,则可以调用 CompletableFuture 实例中的静态方法 completedFuture() ,将结果作为参数传入。 之后调用 Futureget() 方法将不再会阻塞,而是立即返回此结果。

Future  CompletableFuture = CompletableFuture.completedFuture("Hello");

// ... do something else

String result = CompletableFuture.get();
assertEquals("Hello", result);

另一种情况是你可能要取消执行 Future 任务
假设我们没有设法找到计算结果,而是决定完全取消异步执行。 这可以通过 Futurecancel() 方法 来完成。 此方法接收一个 boolean 型参数 mayInterruptIfRunning,但是对于 CompletableFuture 而言这个方法是无效的,因为不使用中断来控制 CompletableFuture 的处理。

这是前面异步方法的修改版本

public Future  calculateAsyncWithCancellation() throws InterruptedException {
    CompletableFuture CompletableFuture = new CompletableFuture<>();
 
    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        CompletableFuture.cancel(false);
        return null;
    });
 
    return CompletableFuture;
}

当我们使用 Futureget() 方法来阻塞并尝试获取结果时,如果 Future 已经被取消执行了,就会抛出CancellationException 异常。

4.封装计算逻辑的 CompletableFuture

上面的代码允许我们选择任意的并行执行机制(Thread 执行或 Runnable 执行或 ThreadPool 执行),但是如果我们想跳过这些样板并简单地异步执行一些代码,该怎么做呢?

静态方法 runAsync()supplyAsync() 允许我们根据 RunnableSupplier 功能类型分别创建 CompletableFuture 实例。

由于新的 Java 8功能, RunnableSupplier 都是 functional 函数式接口,所以允许它们的实例作为 lambda 表达式传递。

Runnable 接口与线程中使用的旧接口相同,并且不允许返回值。
Supplier 接口是具有单个方法的通用 functional 接口,该方法没有参数,并且返回参数化类型的值

这样的特性就允许我们提供 Supplier 的实例作为 lambda 表达式来执行计算并返回结果。这十分简单:

CompletableFuture Future = CompletableFuture.supplyAsync(() -> "Hello");
 
// ... do sometiong else
 
assertEquals("Hello", Future.get());

5.处理异步计算的结果

处理计算结果的最通用方法是将其提供给另一个函数。 thenApply() 方法的作用正是:接受一个 Function 实例,使用这个函数来处理上一个 Future 的计算结果,并返回一个新的 Future ,该 Future 包含一个函数处理后的新值:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello");
 
CompletableFuture Future = completableFuture.thenApply(s -> s + " World");
 
assertEquals("Hello World", Future.get());

如果不需要在 Future 链中返回值,可以改用另一个函数式接口 ConsumerConsumer 中的方法是接受一个参数并返回 void

CompletableFuture 中有一个针对该用例的方法 — thenAccept() 接收一个 Consumer 实例并将计算结果传递给它。最后的 Futureget() 方法调用后返回 void

CompletableFuture CompletableFuture = CompletableFuture.supplyAsync(() -> "Hello");
 
CompletableFuture Future = CompletableFuture.thenAccept(s -> System.out.println("Computation returned: " + s));
 
Future.get();

最后,如果你既不需要计算的值,又不想在 Future 链的末端返回某个值,则可以将 Runnable 通过 lambda 传递给 thenRun() 方法。 在以下示例中,在调用 get() 方法之后,我们仅在控制台中打印出一行 "Computation finished.":

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello");
 
CompletableFuture Future = completableFuture.thenRun(() -> System.out.println("Computation finished."));
 
Future.get();

6. Combining Futures

CompletableFuture API 最棒的部分是能够在一系列计算步骤中组合 CompletableFuture 实例的功能。
这种 Future 链的结果本身就是 CompletableFuture ,它允许进一步的链接和组合。这种做法在函数式编程语言中无处不在。

在以下示例中,我们将使用 thenCompose() 方法按顺序链接两个 Future
请注意,此方法会调用一个函数并返回 CompletableFuture 实例。 此函数的参数是上一个计算步骤的结果。 这使我们可以在下一个 CompletableFuture 的 lambda 中使用该值:

CompletableFuture CompletableFuture = CompletableFuture.supplyAsync(() -> "Hello")
   .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));
 
assertEquals("Hello World", CompletableFuture.get());

thenCompose() 方法与 thenApply() 一起实现了monadic模式的基础构件。它们与Java 8中同样可用的 Streammap()flatMap() 方法紧密相关。

这两个方法都接收一个函数并将其应用于计算结果,但是 thenCompose(flatMap) 方法接收一个函数,该函数返回另一个相同类型的对象。 这种功能结构允许将这些类的实例组成构件。

如果要执行两个独立的 Future 并对其结果进行处理,请使用 thenCombine() 方法,该方法接受带有两个参数的 FutureFunction 来处理两个结果:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(() -> " World"), (s1, s2) -> s1 + s2));
 
assertEquals("Hello World", completableFuture.get());

7. thenApply() 和 thenCompose() 之间的区别

在前面的部分中,我们显示了有关 thenApply()thenCompose() 的示例。这两个方法都可以用来链接不同的 CompletableFuture 调用,但是这两个方法的用法不同。

7.1 thenApply()

这个方法用于处理上一个调用的结果。但是,要记住的关键是返回类型将结合所有调用。

因此,当我们要转换 CompletableFuture 调用的结果时,此方法很有用:

CompletableFuture finalResult = compute().thenApply(s -> s + 1);

7.2 thenCompose()

thenCompose() 方法类似于 thenApply(),两者均返回新的 Completion Stage

但是,thenCompose() 使用上一个 stage 作为参数。它将被 flatten 并直接返回带有结果的 Future ,而不是如thenApply() 中观察到的嵌套的 Future

CompletableFuture computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture finalResult = compute().thenCompose(this::computeAnother);

因此,如果是想要链接多个 CompletableFuture 方法,那么最好使用 thenCompose()

另外注意,这两种方法之间的差异类似于 map()flatMap() 之间的差异。

8. 并行运行多个 Future

当我们需要并行执行多个 Future 时,我们通常要等待所有 Future 执行完成,然后处理它们的合并结果。

CompletableFuture 中的 allOf() 静态方法允许等待以参数形式传入的所有 Future 的完成:

 CompletableFuture Future1 = CompletableFuture.supplyAsync(() -> "Hello");
 CompletableFuture Future2 = CompletableFuture.supplyAsync(() -> "Beautiful");
 CompletableFuture  Future3 = CompletableFuture.supplyAsync(() -> "World");
 
 CompletableFuture  combinedFuture 
     = CompletableFuture.allOf(Future1, Future2, Future3);
 
// ...
 
combinedFuture.get();
 
assertTrue(Future1.isDone());
assertTrue(Future2.isDone());
assertTrue(Future3.isDone());

请注意,allOf() 的返回类型是 CompletableFuture 。 此方法的局限性在于它不会返回所有 Future 的合并结果。 所以当你需要合并多个 Future 结果时,你必须手动进行。 幸运的是, join() 方法和 Java 8 Streams API 使其变得简单:

String combined = Stream.of(Future1, Future2, Future3)
  .map( CompletableFuture ::join)
  .collect(Collectors.joining(" "));
 
assertEquals("Hello Beautiful World", combined);

join() 方法类似于 get() 方法,但是如果 Future 无法正常完成,它将抛出非受检异常。这样就可以将其用作 Stream.map() 方法中的方法引用。

9. 处理错误

为了在一系列异步计算步骤中进行错误处理,必须通过 throw / catch 。与在语法块中使用 try catch 捕获异常不同的是, CompletableFuture 类使你可以使用特殊的 handle() 方法对其进行处理。 此方法接收两个参数:计算结果(如果成功完成)和引发的异常(如果某些计算步骤未正常完成)。

在下面的示例中,我们使用 handle() 方法来处理当缺少 name 参数时导致程序报错的情况,并通过 handle() 方法输出一个默认值:

String name = null;
 
// ...
 
 CompletableFuture   CompletableFuture   
  =   CompletableFuture .supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  })}).handle((s, t) -> s != null ? s : "Hello, Stranger!");
 
assertEquals("Hello, Stranger!",  CompletableFuture .get());

作为另一种方案,假设我们像第一个示例一样,想用一个值手动通过 complet() 完成 Future ,同时也希望能够在出现异常时也能够完成 FuturecompleteExceptionally() 方法就是为此目的而设计的。

以下示例中的 get() 方法将抛出一个 ExecutionException,其内部是 RuntimeException:

 CompletableFuture   CompletableFuture  = new  CompletableFuture <>();
 
// ...
 
 CompletableFuture .completeExceptionally(new RuntimeException("Calculation failed!"));
 
// ...
 
 CompletableFuture .get(); // ExecutionException

在上面的示例中,我们可以使用 handle() 方法异步处理异常,但是另一种方式通过 get() 方法,我们也可以实现典型的同步异常处理。

10. 异步方法

CompletableFuture 类中的大多数 API 方法都有两个额外的类似的方法,带有 Asyn 后缀。这些方法通常用于在另一个线程中运行相应的执行步骤。

没有 Asyn 后缀的方法使用当前线程运行下一个执行阶段。

不带 Executor 参数的 Async 方法运行一个步骤,该步骤使用通过 ForkJoinPool.commonPool() 方法访问的 Executor 的线程池实现。 具有 Executor 参数的 Async 方法使用传递的 Executor 运行一个步骤。

这是一个经过修改的示例,该示例使用 Function 实例处理计算结果。 唯一可见的区别是 thenApplyAsync() 方法。 但是在幕后,函数的应用程序包装到了 ForkJoinTask 实例中。 这可以使你的计算更加并行化,并可以更有效地使用系统资源。

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello");
 
CompletableFuture Future = completableFuture
    .thenApplyAsync(s -> s + " World");
 
assertEquals("Hello World", Future.get());

你可能感兴趣的:(CompletableFuture入门)