龙空技术网

Java 异步编程的完美利器:CompletableFuture 指南

小心程序猿QAQ 1807

前言:

而今小伙伴们对“java组合编程”大体比较关切,小伙伴们都想要分析一些“java组合编程”的相关资讯。那么小编在网络上汇集了一些关于“java组合编程””的相关知识,希望朋友们能喜欢,看官们快快来了解一下吧!

Future获取异步执行结果

之前我们详细探索了线程池,在上一篇文章中,我们仅仅介绍了 ThreadPoolExecutor 的 void execute(Runnable command) 方法,利用这个方法虽然可以提交任务,但是却没有办法获取任务的执行结果(execute() 方法没有返回值)。而很多场景下,我们又都是需要获取任务的执行结果的。

Future介绍

Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。下面我们先来介绍这 3 个 submit() 方法,这 3 个方法的方法签名如下。

scss复制代码// 提交Runnable任务Future<?> submit(Runnable task);// 提交Callable任务<T> Future<T> submit(Callable<T> task);// 提交Runnable任务及结果引用  <T> Future<T> submit(Runnable task, T result);

我们发现它们的返回值都是 Future 接口。

Future 接口有 5 个方法:

取消任务的方法 cancel()判断任务是否已取消的方法 isCancelled()判断任务是否已结束的方法 isDone()获得任务执行结果的 get() 和 get(timeout, unit),其中最后一个 get(timeout, unit) 支持超时机制。

下面我们简单看下Future的例子:

arduino复制代码public class FutureExample {    public static void main(String[] args) throws InterruptedException, ExecutionException {        ExecutorService executor = Executors.newFixedThreadPool(2);        Future<Integer> future = executor.submit(() -> {            // 模拟耗时计算            Thread.sleep(2000);            return 2 + 3;        });        System.out.println("异步计算中...");        // 阻塞等待计算结果        int result = future.get();        System.out.println("计算结果: " + result);        executor.shutdown();    }}
FutureTask

FutureTask是一个实现了RunnableFuture接口的类,它既可以作为Runnable对象传递给线程执行,也可以作为Future对象获取任务的结果。因此,我们可以通过FutureTask将Callable任务转化为可执行的异步任务,并在需要时获取任务的结果。

下面是一个FutureTask的例子:

arduino复制代码public class FutureTaskExample {    public static void main(String[] args) throws InterruptedException, ExecutionException {        Callable<Integer> callable = () -> {            // 模拟耗时计算            Thread.sleep(2000);            return 2 + 3;        };        FutureTask<Integer> futureTask = new FutureTask<>(callable);        // 创建线程执行任务        Thread thread = new Thread(futureTask);        thread.start();        System.out.println("异步计算中...");        // 阻塞等待计算结果        int result = futureTask.get();        System.out.println("计算结果: " + result);    }}

FutureTask也可以直接作为ExecutorService的参数进行提交,以便执行任务,而无需手动创建线程。这样可以更方便地管理线程池和异步任务。

Future与FutureTask的不足

尽管Future与FutureTask在Java中提供了一种基本的异步编程方式,但它也存在一些不足之处:

缺乏异步回调机制:FutureTask和Future接口都没有直接提供异步回调的机制。在某些场景下,我们可能希望在任务完成后立即执行一些操作,而不是阻塞等待结果。需要手动编写额外的代码来实现异步回调逻辑,增加了代码的复杂性。无法手动完成或取消任务:FutureTask和Future都没有提供主动完成或取消任务的方法。一旦任务提交,就无法在外部控制其执行状态。这可能会导致无法优雅地管理任务的生命周期和资源。阻塞式获取结果:在使用get()方法获取结果时,如果任务还未完成,调用线程会被阻塞,无法进行其他操作。这种阻塞式获取结果的方式可能导致整体性能下降,特别是在多个异步任务同时执行时。缺乏异常处理灵活性:Future和FutureTask在处理任务执行过程中的异常时,比较简单且不够灵活。通过捕获ExecutionException来获取异常信息,可能需要额外的处理逻辑来处理不同类型的异常情况。

为了解决这些问题,Guava提供了ListenableFuture,Java 8引入了CompletableFuture,它们都提供了更丰富的功能和灵活性,如异步回调、异常处理、任务组合等。

CompletableFuture使用CompletableFutur介绍

CompletableFuture提供下面几种方法创建任务,它们之间的区别是:Runnable 接口的 run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。

scss复制代码//使用内置线程ForkJoinPool.commonPool(),根据supplier构建执行任务public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {      return asyncSupplyStage(asyncPool, supplier);  }  //指定自定义线程,根据supplier构建执行任务public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,  Executor executor) {      return asyncSupplyStage(screenExecutor(executor), supplier);  }  //使用内置线程ForkJoinPool.commonPool(),根据runnable构建执行任务public static CompletableFuture<Void> runAsync(Runnable runnable) {      return asyncRunStage(asyncPool, runnable);  } //指定自定义线程,根据runnable构建执行任务public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor) {      return asyncRunStage(screenExecutor(executor), runnable); }

默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。

如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以我们要根据不同的业务类型创建不同的线程池,以避免互相干扰。

下面是一个CompletableFuture获取异步结果的例子:

arduino复制代码public class CompletableFutureExample {    public static void main(String[] args) throws InterruptedException, ExecutionException {        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {            // 模拟耗时计算            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            return 2 + 3;        });        System.out.println("异步计算中...");        future.thenAccept(result -> System.out.println("计算结果: " + result));        // 阻塞等待任务完成        future.get();    }}
时序依赖关系

通常任务的依赖分为以下几种:

串行执行

Start

Future1

Future2

Future3

End

所有都执行完:

Start

Future1

Future2

Future3

Converge

End

任意完成:

Start

Future1

Future2

Future3

Converge

End

CompletableFuture的CompletionStage 接口可以清晰地描述任务之间的这种时序依赖关系。下面我们看下CompletionStage 接口如何描述串行关系、AND 聚合关系、OR 聚合关系以及异常处理。

CompletionStage编排串行执行

描述串行关系CompletionStage 接口里面描述串行关系,主要是 thenApply、thenAccept、thenRun 和 thenCompose 这四种类型的接口。

thenApply 系列函数里参数 fn 的类型是接口 Function,这个接口里与 CompletionStage 相关的方法是 R apply(T t),这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是CompletionStage。thenAccept 系列方法里参数 consumer 的类型是接口Consumer,这个接口里与 CompletionStage 相关的方法是 void accept(T t),这个方法虽然支持参数,但却不支持回值,所以 thenAccept 系列方法返回的是CompletionStage。thenRun 系列方法里 action 的参数是 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是CompletionStage。这些方法里面 Async 代表的是异步执行 fn、consumer 或者 action。thenCompose 方法,这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。

scss复制代码CompletionStage thenApply(fn);CompletionStage thenApplyAsync(fn);CompletionStage thenAccept(consumer);CompletionStage thenAcceptAsync(consumer);CompletionStage thenRun(action);CompletionStage thenRunAsync(action);CompletionStage thenCompose(fn);CompletionStage thenComposeAsync(fn);

通过下面的示例代码,我们可以看一下 thenApply() 方法是如何使用的。首先通过 supplyAsync() 启动一个异步流程,之后是两个串行操作,虽然这是一个异步流程,但任务①②③却是串行执行的,②依赖①的执行结果,③依赖②的执行结果。

rust复制代码CompletableFuture<String> f0 =   CompletableFuture.supplyAsync(    () -> "Hello World")      //①  .thenApply(s -> s + " 你好!")  //②  .thenApply(String::toUpperCase);//③System.out.println(f0.join());
AND汇聚关系

CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。

scss复制代码CompletionStage<R> thenCombine(other, fn);CompletionStage<R> thenCombineAsync(other, fn);CompletionStage<Void> thenAcceptBoth(other, consumer);CompletionStage<Void> thenAcceptBothAsync(other, consumer);CompletionStage<Void> runAfterBoth(other, action);CompletionStage<Void> runAfterBothAsync(other, action);
OR 汇聚关系

CompletionStage 接口里面描述 OR 汇聚关系,主要是 applyToEither、acceptEither 和 runAfterEither 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。

scss复制代码CompletionStage applyToEither(other, fn);CompletionStage applyToEitherAsync(other, fn);CompletionStage acceptEither(other, consumer);CompletionStage acceptEitherAsync(other, consumer);CompletionStage runAfterEither(other, action);CompletionStage runAfterEitherAsync(other, action);

下面的示例代码展示了如何使用 applyToEither() 方法来描述一个 OR 汇聚关系。

ini复制代码CompletableFuture<String> f1 =   CompletableFuture.supplyAsync(()->{    int t = getRandom(5, 10);    System.sleep(t, TimeUnit.SECONDS);    return String.valueOf(t);});CompletableFuture<String> f2 =   CompletableFuture.supplyAsync(()->{    int t = getRandom(5, 10);    System.sleep(t, TimeUnit.SECONDS);    return String.valueOf(t);});CompletableFuture<String> f3 =   f1.applyToEither(f2,s -> s);System.out.println(f3.join());
异常处理

异常处理虽然上面我们提到的 fn、consumer、action 它们的核心方法都不允许抛出可检查异常,但是却无法限制它们抛出运行时异常。正常业务代码中,我们可以使用 try{}catch{}来捕获并处理异常,那在异步编程里面,异常该如何处理呢?

ini复制代码CompletableFuture f0 = CompletableFuture.supplyAsync(()->(3/0))     .thenApply(r->r*10);System.out.println(f0.join());

CompletionStage 接口给我们提供的方案非常简单,比 try{}catch{}还要简单,下面是相关的方法,使用这些方法进行异常处理和串行操作是一样的,都支持链式编程方式。

scss复制代码CompletionStage exceptionally(fn);CompletionStage whenComplete(consumer);CompletionStage whenCompleteAsync(consumer);CompletionStage handle(fn);CompletionStage handleAsync(fn);

下面的示例代码展示了如何使用 exceptionally() 方法来处理异常,exceptionally() 的使用非常类似于 try{}catch{}中的 catch{},但是由于支持链式编程方式,所以相对更简单。既然有 try{}catch{},那就一定还有 try{}finally{},whenComplete() 和 handle() 系列方法就类似于 try{}finally{}中的 finally{},无论是否发生异常都会执行 whenComplete() 中的回调函数 consumer 和 handle() 中的回调函数 fn。whenComplete() 和 handle() 的区别在于 whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。

kotlin复制代码CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {    // 模拟一个可能抛出异常的异步操作    if (Math.random() < 0.5) {        throw new RuntimeException("Something went wrong");    }    return 10;});CompletableFuture<Integer> handleException = future.exceptionally(ex -> {    // 异常处理逻辑    System.out.println("Exception occurred: " + ex.getMessage());    return 0; // 返回默认值或处理后的结果});handleException.thenAccept(result -> {    // 在最终结果完成后进行处理    System.out.println("Final result: " + result);});
获取结果

CompletableFuture提供了下面几种常用的方法获取结果:

java复制代码public  T   get()public  T   get(long timeout, TimeUnit unit)public  T   getNow(T valueIfAbsent)public  T   join()public CompletableFuture<T> allOf()public CompletableFuture<T> anyOf()public CompletableFuture<T> whenComplete()public <U> CompletableFuture<U> handle()
get():get()方法是最常用的获取CompletableFuture结果的方法之一。它会阻塞当前线程,直到异步操作完成并返回结果,或者抛出异常。如果异步操作抛出异常,get()方法会将异常包装在ExecutionException中抛出。这个方法可以用于同步地获取结果。join():join()方法与get()方法类似,也会阻塞当前线程,直到异步操作完成并返回结果,或者抛出异常。不同之处在于,join()方法不会抛出ExecutionException,而是直接将异常抛出。这个方法可以用于同步地获取结果。whenComplete(BiConsumer<? super T,? super Throwable> action):whenComplete()方法允许注册一个回调函数,在异步操作完成后执行该函数。回调函数接收异步操作的结果(如果成功完成)或异常(如果发生异常),并可以对结果进行进一步处理或执行其他操作。这个方法不会阻塞线程,异步操作完成后立即执行回调函数。handle(BiFunction<? super T, Throwable, ? extends U> fn):handle()方法类似于whenComplete(),也允许注册一个回调函数,在异步操作完成后执行该函数。不同之处在于,回调函数的返回值会被包装在新的CompletableFuture中返回,而不是忽略返回值。这个方法可以用于对结果进行处理或转换,并返回包装后的新CompletableFuture。allOf():就是所有任务都完成时触发。allOf()可以配合get()一起使用。anyOf():等待任意一个完成。anyOf()方法返回一个新的CompletableFuture<Object>对象,该对象在任意一个输入的CompletableFuture完成后完成,并持有该完成的结果。

下面是allOf()的一个例子

ini复制代码CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Hello");CompletableFuture<Double> future3 = CompletableFuture.supplyAsync(() -> 3.14);CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2, future3);allFutures.thenRun(() -> {    System.out.println("All futures completed");    Integer result1 = future1.join();    String result2 = future2.join();    Double result3 = future3.join();    System.out.println("Result 1: " + result1);    System.out.println("Result 2: " + result2);    System.out.println("Result 3: " + result3);});

作者:半亩方塘立身

链接:

标签: #java组合编程