2023-03-22  阅读(2)
原文作者:简单教程 原文地址:https://www.twle.cn/c/yufei/javatm/javatm-basic-executorservice.html

本文我们来了解下 Java 8 引入的 CompletableFuture 类,了解下该类提供的功能和用例。

Java 中的异步计算

异步计算很难推理的,因为我们的大脑是同步的,会将任何计算看成是一系列的同步计算。

我们在实现异步计算时,往往会把回调的动作分散在代码中或者深深地嵌套在彼此内部,这种情况下,当我们需要处理其中一个步骤中可能发生的错误时,情况变得更糟。

人生的一大悲剧是,尽管 Java 5 已经看到了这种恶性循环,提供了Future 接口作为异步计算的结果,但它没有提供任何方法来组合这些计算或处理可能的错误。

直到 Java 8,才引入了 CompletableFuture 类。该类不仅实现了 Future 接口,还实现了 CompletionStage 接口。此接口定义了可与异步计算步骤组合的异步计算步骤契约。

官方文档真是拗口,简单来说,CompletionStage 接口规范了一个异步计算步骤如何与另一个异步计算步骤组合。

CompletableFuture 类还是一个集大成者,即是一个构建块,也是一个框架,提供了大约 50 种不同的方法来构造,组合,执行异步计算步骤和处理错误。

API 数量如此之多,第一眼看到简直就傻眼了,不过好在它们可以分门别类,因为它们大多属于几个明确且不同的用例。

将 CompletableFuture 当作简单的 Future 使用

为什么可以 ?

因为 CompletableFuture 类实现了 Future 接口,因此我们可以将其用作 Future 实现,但需要自己实现额外的完成逻辑。

例如,我们可以使用无任何参数的构造函数来创建此类的实例,用于表示未来的某些结果,然后将其交给使用者,并在将来的某个时间调用 complete() 方法完成。消费者可以使用 get() 方法来阻止当前线程,直到提供此结果。

    public Future<String> calculateAsync() throws InterruptedException {
        CompletableFuture<String> completableFuture 
          = new CompletableFuture<>();
    
        Executors.newCachedThreadPool().submit(() -> {
            Thread.sleep(500);
            completableFuture.complete("Hello");
            return null;
        });
    
        return completableFuture;
    }

上面的实例中,我们创建了一个创建 CompletableFuture 实例的方法,把计算分离到另一个线程中并立即返回 Future。当计算完成后,该方法通过将结果提供给 complete() 方法来完成 Future。

为了分离计算,我们使用了前几章节 一文秒懂 Java 线程池 ( Thread Pool ) (上) 中提到的 Executor API。这种创建和完成 CompletableFuture 的方法可以与任何并发机制或 API( 包括原始线程 )一起使用。

请注意,calculateAsync() 方法返回 Future 实例。

接下来,我们只要调用此方法,接收 Future 实例并在我们准备阻塞结果时调用它的 get() 方法。

    Future<String> completableFuture = calculateAsync();
    
    // ... 
    
    String result = completableFuture.get();
    assertEquals("Hello", result);

注意: get() 方法会抛出一些已检查的异常,即 ExecutionException( 封装计算期间发生的异常 )和 InterruptedException( 表示执行方法的线程被中断的异常 )。

如果你已经知道计算的结果,可以将表示此计算的结果作为参数传递给 completedFuture() 静态方法,这样,Future 的 get() 方法永远不会阻塞,而是立即返回此结果。

    Future<String> completableFuture = 
      CompletableFuture.completedFuture("Hello");
    
    // ...
    
    String result = completableFuture.get();
    assertEquals("Hello", result);

当然了,有时候,你可能希望取消 Future 的执行。

假设我们没有找到结果并决定完全取消异步执行,这可以通过调用 Future 的 cancel() 方法完成。此方法接收一个布尔参数 mayInterruptIfRunning

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

在上面这个异步方法的修改版本的范例中,当我们使用 Future.get() 方法阻塞结果时,如果 future 取消了,那么将抛出CancellationException 异常。

但是,在类型为 CompletableFuture 的情况下,cancel() 方法没有任何效果,因为 CompletableFuture 并不会响应中断也不会处理中断。

用于封装计算逻辑的 CompletableFuture

上面讲解的这些代码,都允许我们选择任何并发执行机制,但是,如果我们想跳过这个样板代码并简单地异步执行一些代码呢?

CompletableFuture 的静态方法 runAsync()supplyAsync() 允许我们从 Runnable 和 Supplier 中创建 CompletableFuture 实例。 Runnable 和 Supplier 都是功能接口,由 Java 8 的新功能,可以将它们的实例作为lambda 表达式传递:

  • Runnable 接口与线程中使用的旧接口相同,不允许返回值
  • Supplier 接口是一个通用的功能接口,只有一个没有参数的方法,并返回一个参数化类型的值

下面的代码演示了如何将 Supplier 的实例作为 lambda 表达式参数,该表达式执行计算并返回结果

    CompletableFuture<String> future
      = CompletableFuture.supplyAsync(() -> "Hello");
    
    // ...
    
    assertEquals("Hello", future.get());

处理异步计算的结果

处理计算结果的最通用方法是将其提供给函数。CompletableFuture.thenApply() 方法就是这样做的: 接受一个 Function 实例,用它来处理结果并返回一个用于保存函数返回的值 Future

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

如果你不需要在 Future 链中返回值,则可以使用 Consumer 功能接口的实例。它只有一个方法,该方法接受一个参数并返回 void。而相应的,CompletableFuture 也提供了一个使用 Consumer 实例的方法 thenAccept() 。该方法接收一个 Consumer 并将其传递给计算结果。

    CompletableFuture<String> completableFuture
      = CompletableFuture.supplyAsync(() -> "Hello");
    
    CompletableFuture<Void> future = completableFuture
      .thenAccept(s -> System.out.println("Computation returned: " + s));
    
    future.get();

上面这个示例中,最后的 future.get() 方法会返回空值 Void

最后,如果你既不需要计算的值也不想在链的末尾返回一些值,那么你可以将 Runnable lambda 传递给 thenRun() 方法。

    CompletableFuture<String> completableFuture 
      = CompletableFuture.supplyAsync(() -> "Hello");
    
    CompletableFuture<Void> future = completableFuture
      .thenRun(() -> System.out.println("计算完成"));
    
    future.get();

上面这个示例中,调用 future.get() 方法之后会在控制台打印一行 计算完成


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文