CompletableFuture全面剖析

1,508 阅读16分钟

PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

    首先容我阴阳怪气一番,"不会吧,不会吧,都2022年了,不会还有人使用Future接收异步处理结果吧?🧐" (其实我也是因为在CR时被指出了代码中Future使用会出现的问题,为了强化代码质量,才学习CompletableFuture的 🤓)
好了,言归正传,春节将近,正值辞旧迎新之时,让我们来"喜新厌旧"吧----抛弃Future,投入CompletableFuture的怀抱吧。

一、Future类用法与局限性

1.1 用法

经典面试题:创建线程有几种方法?
其中第三种方法是通过实现Callable接口来创建线程,需要实现其中的call()方法,该call()方法作为线程执行体,并且有返回值。

public interface Callable<V> {
    V call() throws Exception;
}

返回值的获取,需要使用FutureTask类包装获取,它是一个包装类,同时实现了Runnable和Future接口,获取结果实际上就是用到了Future接口。

Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "test";
            }
        };
        //包装返回值
        FutureTask<String> future = new FutureTask<>(callable);
        //启动子线程
        new Thread(future).start();
        //打印子线程回调结果
        System.out.println(future.get());

1.2 介绍

Future是JAVA 1.5提供的接口,结合Callable使用,用于在线程执行完毕之后得到任务执行结果,代表一个未来能获取结果的对象。因为可以异步获得子线程执行结果,所以子线程不用一直同步等待回调结果。
上面的例子中只用到了最基本的get()方法来获取结果,Future一共定义了如下5个方法。 Future.png

解释:

  • cancel(boolean):可以取消这个执行逻辑,如果这个逻辑已经正在执行,提供可选的参数来控制是否取消已经正在执行的逻辑;
  • get():获取执行逻辑的执行结果;
  • get(long,TimeUnit):可以允许在一定时间内去等待获取执行结果,如果超过这个时间,抛TimeoutException
  • isCancelled():判断执行逻辑是否已经被取消;
  • isDone():判断执行逻辑是否已经执行完成。

1.3 局限性

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。并且多个任务,轮询get(),使用get(long,TimeUnit)方法限制超时时间,实际只是限制每个任务的时间,不能做到限制所有任务都完成的耗时时间。如以下案例:

ExecutorService executor = Executors.newFixedThreadPool(4);

        Callable<String> task1 = () -> {
            Thread.sleep(200);
            return "first";
        };

        Callable<String> task2 = () -> {
            Thread.sleep(400);
            return "second";
        };

        Future<String> future1 = executor.submit(task1);
        Future<String> future2 = executor.submit(task2);
        long now = System.currentTimeMillis();
        System.out.println(future1.get(200, TimeUnit.MILLISECONDS));
        System.out.println(future2.get(200, TimeUnit.MILLISECONDS));

        System.err.println("整体耗时,cost=" + (System.currentTimeMillis() - now));

定义了两个任务,任务1耗时200ms,任务2耗时400ms,获取回调结果限制时间都为200ms,实际运行结果:

可以看到任务2没有抛出TimeoutException,总耗时远远超过200ms,如果上层调用方限制服务时间不可超过200ms,则整个调用都会因为超时被抛弃,实际上任务1的结果是可以正常返回的。
截屏2022-01-07 下午4.07.24.png
具体解释如下图所示,任务1和任务2是并发执行的,但是获取关系是先后进行,任务1首先执行get(),执行的过程中任务2同时进行,任务2的get()起点是在任务1get()完成点,因此任务2的实际执行时间为任务1执行时间+任务2get时间段。 未命名文件(4).png

二、CompletableFuture介绍与用法

2.1 介绍

CompletableFuture是JAVA 8引入的Future的实现类,针对Future做了改进,当异步任务完成或者发生异常时,自动调用回调对象的回调方法,并且提供了函数式编程的能力。CompletableFuture还实现了另外一个接口—CompletionStage,CompletableFuture的众多方法以及函数式能力都是这个接口赋予的。 CompletionStage.png

2.2 常用方法

CompletableFuture中的方法众多,下面将其分为几种类型分别介绍。

2.2.1 创建

//底层实现就是使用new关键词创建一个CompletableFuture
public static <U> CompletableFuture<U> completedFuture(U value);

//创建不带返回值的CompletableFuture,使用默认的ForkJoinPool.commonPool()作为线程池
public static CompletableFuture<Void> runAsync(Runnable runnable)
//创建不带返回值的CompletableFuture,使用指定的线程池
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor);
//创建带返回值的CompletableFuture,使用默认的ForkJoinPool.commonPool()作为线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
//创建带返回值的CompletableFuture,使用指定的线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor);

第一个方法是静态辅助方法,用来返回一个已知返回值的completedFuture对象。
下面的四个方法都是用来为一段异步执行的代码创建completedFuture对象。其中『runAsync』和『supplyAsync』区别只有前者没有返回值,后者有返回值;重构方法的不同在于是否指定线程池,没有线程池形参的使用默认的ForkJoinPool.commonPool()作为线程池,相反的是使用指定的线程池。可以看到他们的入参都满足函数式编程,因此可以使用lambda表达式来创建任务,使用案例:(写法可以进一步简化,考虑到可读性这里不做简化处理)

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("hello");
        });

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "hello";
        });

因为现在项目都是基于SpringBoot框架创建的,所以重点介绍下CompletableFuture结合SpringBoot创建线程池的使用方式。
SpringBoot对于线程池的封装使得项目中应用多线程变得得心应手,主要流程为Application增加@EnableAsync注解 —> 创建一个config,注入线程池的相关配置 —> 在需要进行异步处理的方法上加上@Async注解,可以指定要使用的线程池。

💡 SpringBoot是使用`@Async`注解以方法为维度来管理线程,使用过程中发现以下几点注意事项:1、调用的方法和注解方法不可以在同一类中;2、注解方法的位置应该在实现类中,放在接口或抽象类中都会导致线程池失效;3、注解方法不可以进行递归操作,即不可以自己调用自己。

关于SpringBoot使用线程池的用法不做过多展开。
SpringBoot使用CompletableFuture的用法简单,只需要将@Async注解标注方法的返回值设为返回CompletableFuture即可。下面的例子是直接使用静态方法completedFuture()创建对象,也可以尝试使用supplyAsync(supplier, executor)方法,但是注意需要指定线程池,即需要将SpringBoot创建的线程池自动注入后传入到方法中。

@Async(value = "testThreadPool")
public CompletableFuture<String> handle() {
       return CompletableFuture.completedFuture("hello");
}

2.2.2 获取结果

//返回计算的结果或者抛出一个unchecked异常(CompletionException)
public T join();
//如果任务执行成功则返回结果,结果为null则返回设定的valueIfAbsent;执行失败抛出异常
public T getNow(T valueIfAbsent);

//手动设定任务返回值,只能执行一次
public boolean complete(T value);
//手动抛一个设定的异常
public boolean completeExceptionally(Throwable ex);

因为是Future的实现类,实际上『得到结果』还包含Future中的两个get()方法。
由于join()getNow()的使用比较简单,因此这里只举例complete()的使用。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {
            }

            return "hello";
        });

        System.out.println("任务是否完成:" + future.isDone());
        System.out.println("手动设置值是否成功:" + future.complete("bye"));
        System.out.println("任务是否完成:" + future.isDone());
        System.err.println("任务返回值:" + future.get());

结果为:
截屏2022-01-08 下午6.17.33 (1).png

例子中让线程阻塞100ms,是为了让程序进行到complete()时,任务还未执行完成,这时complete()的返回值才是true,并且任务判定为完成,可以看到get()的结果为设定的值;如果将sleep去掉,结果为:
截屏2022-01-08 下午7.01.18.png
可以看到在complete()之前任务就已经完成,此时再进行设定值的操作会返回false,意为操作失败,这也是为什么complete()只可以执行一次的原因。
completeExceptionally()complete()使用方式和逻辑相同,这里不做赘述。

2.2.3 处理结果

//任务执行完成后调用给定的操作,可以处理正常的计算结果,也可以处理异常情况
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);
//使用其他的线程执行后续操作,其他线程来源默认线程池
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action);
//使用其他的线程执行后续操作,其他线程来源指定线程池
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor);

//除了可以在任务执行完成后调用设定操作,也可以对任务抛出的异常进行处理
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);

//专门处理异常情况,类似catch关键字
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);

1、首先来看whenComplete方法,可以看到有三个方法,第一个方法whenComplete(action)是普通使用情况,使用完成任务的线程来处理后续设定操作,第二个方法whenCompleteAsync(action)是使用其他线程来处理后续设定操作,没有指定线程池,因此使用默认的ForkJoinPool.commonPool()作为线程池,第三个方法whenCompleteAsync(action , executor)和第二个方法唯一的不同就是指定了线程池。CompletableFuture中有很多方法命名都是相同的逻辑,如后面的handle()thenApply()方法,因此后续的方法介绍只选普通情况,也就是使用完成任务的线程来处理后续设定操作的方法。

💡 需要注意一点的是,使用后两种方法,如果任务执行的线程和后续操作的线程用的是同一个线程池,可能会出现使用同一个线程的情况。是否要用同一线程池需要根据实际业务场景来判断,如果两个操作没有依赖关系,使用同一线程池没问题;如果后续操作中包含另一个CompletableFuture任务,且该任务依赖于前面任务的回调结果,使用同一线程池可能出现死锁问题。并且如果多个CompletableFuture任务使用同一线程池,线程池核心线程数被用光后会持续等待,也会导致死锁的问题,造成服务阻塞。

具体可查看资料:spring自带线程池使用不当导致的死锁问题很隐蔽的一个线程池死锁问题CompletableFuture

2、whenComplete()handle()的区别,从入参来看,一个是BiConsumer类型,一个是BiFunction类型,前者可以传入一个数据和一个异常,后者可以传入两个数据和一个异常;从返回值来看,前者返回的类型与传入的数据类型相同,后者与传入的第二个数据类型相同;从处理方式来看,handle()whenComplete()多了处理异常的功能,相当于是exceptionally()whenComplete()的组合。

  • whenComplete()的使用样例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            int a = 100 / 0;
            return "hello";
        });

CompletableFuture<String> complete = future.whenComplete((v, e) -> {
            System.out.println(v);
            if (v == null) {
                System.out.println("0");
            } else {
                System.out.println("2022");
            }

        });

System.out.println(complete.join());

结果:

截屏2022-01-11 上午11.06.23.png 任务执行遇到异常时,whenComplete()中如果没有对e进行接收处理,后续的『得到结果』操作也会报错。并且whenComplete()无法显式的进行return,返回值都为v。

  • exceptionally()whenComplete()的组合使用
    如果想要正常使用,需要组合exceptionally()使用,如下:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            int a = 100 / 0;
            return "hello";
        });

CompletableFuture<String> complete = future
                .exceptionally(e -> {
                    System.out.println(e.getMessage());
                    return "异常";
                })
                .whenComplete((v, e) -> {
                    System.out.println(v);
                    if (v == null) {
                        System.out.println(0);
                    } else {
                        System.out.println(2022);
                    }
                });

        System.out.println(complete.join());

结果:
截屏2022-01-11 上午11.27.05.png
如何将组合顺序互换,即将exceptionally()放到下面,结果: 截屏2022-01-11 上午11.30.41.png
可以看到与catch关键字功能相同,放在前面遇到异常时可以返回默认值,保证whenComplete()顺利执行,放在后面遇到异常时可以保底。

  • handle()的使用
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            int a = 100 / 0;
            return "hello";
        });

CompletableFuture<Integer> complete = future
                .handle((v, e) -> {
                    System.out.println(v);   //打印:null
                    if (v == null) {
                        return 0;
                    } else {
                        return 2020;
                    }
                });

System.out.println(complete.join());    //结果:0

就像前面提到的,handle()相当于是exceptionally()whenComplete()的组合,自带处理异常的能力,并且可以显式返回数据,而且数据类型可以不做限制。

2.2.4 转换

//指定后续动作,对返回的结果进行处理或改变,并返回执行结果
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);
//执行后无返回结果
public CompletableFuture<Void> thenAccept(Consumer<? super T> action);
//无法获取返回的结果,且执行后无返回结果
public CompletableFuture<Void> thenRun(Runnable action);

三个方法的形参分别为Function、Consumer、Runnable。

  • Function:T为入参,U为返回值;

  • Consumer:T为入参,没有返回值;

  • Runnable:没有入参,没有出参。

    这也是三个方法的区别: | 特点 | thenApply | thenAccept | thenRun | | --- | --------- | ---------- | ------- | | 入参 | 有 | 有 | 无 | | 返回值 | 有 | 无 | 无

只介绍下thenApply的用法,作用是将上一个任务的结果应用于当前任务,并返回处理后的值,数值类型可不同。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2022);

CompletableFuture<String> apply = future
                .thenApply((t) -> "hello " + t)
                .thenApply((t) -> t + "!");

System.out.println(apply.join());  //结果:hello 2022!

从例子中看出,thenApply()可以进行流式调用,可以进行多个任务的串行执行。这三个方法也分别有加Async后缀的兄弟方法,区别在前面提到过—是否使用同一线程以及是否使用指定线程池。

2.2.5 组合

//当两个任务任意一个完成,执行设定的动作,入参为完成任务的返回结果,对返回的结果进行处理或改变,执行后返回结果
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn);
//当两个任务任意一个完成,执行设定的动作,入参为完成任务的返回结果,执行后无返回结果
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action);
//当两个任务都完成时才执行设定的动作,入参为两个任务的返回结果,执行后无返回结果
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
//当两个任务任意一个完成,无法获取任务返回的结果,执行后无返回结果
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action);
//当两个任务都完成时才执行设定的动作,无法获取任务返回的结果,执行后无返回结果
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action);

//两个任务并行执行,完成后执行设定动作,执行后返回结果
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);

//设定动作,入参为调用方CompletableFuture的返回结果,返回值为新的CompletableFuture对象
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
  • 前5个方法
    前5个方法可以依次对标『转换』中三个方法,从方法名中的apply、accept、run关键字可以看出,相当于三个方法针对两个任务分别增加了一个both(全部完成),either(任意完成)的兄弟方法。至于为什么没有thenApplyBoth()方法,猜测是因为前面提到了thenApply()可以进行流式调用,使用以下的方式就能做到”顺序固定”的both功能。accept、run虽然也可以进行流式调用,但是没有返回值,无法做到结果值传递,体现不出两个任务之间的关联关系(只有先后关系)。
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> "hello");
CompletableFuture<Integer> second = CompletableFuture.supplyAsync(() -> 2020);

CompletableFuture<String> apply = first
                .thenApply((t) -> t + " " + second.join()) //可能会造成阻塞
                .thenApply((t) -> t + "!");

System.out.println(apply.join());  //结果:hello 2020!

=======================================================================

public static void main(String[] args) {
        CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> "hello ");
        CompletableFuture<CompletableFuture<String>> apply = first
                .thenApply((t) -> joining(t, 2020));

        System.out.println(apply.join().join());//需要获取两次,结果:hello 2020
    }

    public static CompletableFuture<String> joining(String str, Integer num) {
        return CompletableFuture.completedFuture(str + num);
    }

1、applyToEither的用法。

CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {
            }
            return "hello";
        });

CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "hi");

CompletableFuture<String> applyToEither = first.applyToEither(second, i -> i + "!");

System.out.println(applyToEither.get(50, TimeUnit.MILLISECONDS));  //结果:hi!

可以看到两个任务被applyToEither()组合为新的任务,设定超时时间为50ms,任务1设置阻塞100ms,也并没有报错,正常返回任务2的结果。
2、thenAcceptBoth用法。

CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {
            }
            return "hello ";
        });
CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "2022");

CompletableFuture<Void> applyToEither = first.thenAcceptBoth(second, (t, u) -> System.out.println(t + u + "!"));

//applyToEither.get();  //结果:hello 2022!
applyToEither.get(50, TimeUnit.MILLISECONDS); //超时异常:java.util.concurrent.TimeoutException
  • thenCombine方法 两个任务并行执行,都完成后执行设定动作,入参为两个任务的返回结果,执行后返回结果。简单来讲功能类似thenAcceptBoth(),多了可以返回数据的功能。再直白点说,这不就是thenApplyBoth()方法吗?试验下:
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {
            }
            return "hello ";
        });

CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "2022");

CompletableFuture<String> combine = first.thenCombine(second, (t, u) -> t + u + "!");

//System.out.println(combine.get();  //结果:hello 2022!
System.out.println(combine.get(50, TimeUnit.MILLISECONDS));//超时异常:java.util.concurrent.TimeoutException

事实证明,我上面关于为什么没有thenApplyBoth()方法的推理全部翻车,真正原因就是这个方法取了个别名而已!! 🌚
言归正传,虽然这里动作的形参也是BiFunction,和handle()的形参相同,但是注意handle的泛型为<? super T, Throwable, ? extends U>:参数1为前一个任务的返回值,参数2为异常,参数3为动作返回的数据,这里的泛型为<? super T,? super U,? extends V>,1、3参数含义相同,只是参数2表示另一个任务的返回值,因此不要认为thenCombine()也具备处理异常的能力。

  • thenCompose方法 对调用的CompletableFuture任务进行加工,返回一个新的CompletableFuture任务,返回值类型可以修改。使用方式:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello ");

CompletableFuture<String> compose = future.thenCompose((t) -> CompletableFuture.completedFuture(t + "2022!"));

System.out.println(compose.join());  //结果:hello 2022!

可以看到使用方式比较简单,需要关注的点是它与thenApply()的区别,也是『组合』与『转换』的区别,thenApply()相当于将CompletableFuture转换成CompletableFuture,改变的是同一个CompletableFuture中的返回值,包括泛型类型;thenCompose()用来连接两个CompletableFuture,返回值是一个新的CompletableFuture。通过上面thenApply()使用样例,也可以看出thenApply()适用于直接对返回值进行操作,如果涉及到两个任务的处理,可能会出现”阻塞”、”多层嵌套”的问题。

2.2.6 辅助方法

//多个任务全部执行完成后执行计算,无返回值
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
//多个任务中任一任务完成执行计算,返回完成任务的结果
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);

上面提到『组合』中的几个方法都是针对两个任务而言,如果想要将多个任务组合起来,就需要用到allOf()anyOf()方法。allof()的作用与CountDownLatch类相似,即等待多个线程全部完成才能进行后续操作,使用也都是一次性的。

  • allOf使用案例
//生成几个任务
List<CompletableFuture<String>> futures = Stream.of("Tom", "Mack", "Black")
                .map(name -> CompletableFuture.supplyAsync(() -> "hello " + name))
                .collect(Collectors.toList());
//完成任务
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                .whenComplete((t, e) -> {
                    futures.forEach((future) -> {
                        System.out.println(future.join());
                    });
                });

//结果:
//hello Tom
//hello Mack
//hello Black
  • anyOf使用案例
List<CompletableFuture<String>> futures = Stream.of("Tom", "Mack", "Black")
                .map(name -> CompletableFuture.supplyAsync(() -> "hello " + name))
                .collect(Collectors.toList());

CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]))
                .whenComplete((t, e) -> {
                    System.out.println(t);
                });
//结果:
//hello Tom

2.3 组合用法

2.3.1 allOf实现多个任务整体限制超时时间

这里主要对标『1.3』章节Future的局限性,做到每个任务超时互不影响。

List<String> nameList = Arrays.asList("Tom", "Mack", "Black");
List<CompletableFuture<Void>> futureList = Lists.newArrayList();

//定义三个任务,后续动作相同,任务阻塞时间分别为500ms、1000ms、1500ms
for (int index = 0; index < nameList.size(); index++) {
            int finalIndex = index + 1;
            long now = System.currentTimeMillis();
            CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep(500 * finalIndex);
                } catch (InterruptedException ignored) {
                }
                return "hello " + nameList.get(finalIndex - 1);
            }).thenAccept(v -> {
                System.out.println(v);
                System.err.println("任务" + finalIndex + "运行时间=" + (System.currentTimeMillis() - now));
            });
            futureList.add(future);
        }

CompletableFuture<Void> allOf = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));

allOf.get();

运行结果:
截屏2022-01-12 下午5.18.59.png
如果最后给get()加上限制时间,allOf.get(1000, TimeUnit.MILLISECONDS)。 运行结果:
截屏2022-01-12 下午5.25.44.png 可以看到某个任务执行超时或抛出异常不影响其他任务的执行。

2.3.2 多个任务,任一任务失败整体结束

先给出allOf()使用过程中的特性,如下所示:

List<String> nameList = Arrays.asList("Tom", "Mack", "Black");
List<CompletableFuture<Void>> futureList = Lists.newArrayList();

//定义三个任务,后续动作相同,第二个任务抛出异常,为了提现其他任务还在执行,将其他任务都阻塞500ms
for (int index = 0; index < nameList.size(); index++) {
            int finalIndex = index;
            CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
                if (finalIndex == 1) {
                    int a = 100 / 0;
                } else {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException ignored) {
                    }
                }
                return "hello " + nameList.get(finalIndex);
            }).thenAccept(System.out::println);
            futureList.add(future);
        }

CompletableFuture<Void> allOf = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
allOf.join();

运行结果: 截屏2022-01-12 下午7.10.07.png 可以看到,任务2抛出异常后,另外两个任务仍然正常执行完毕。


想要做到任一任务失败整体结束,需要组合exceptionally()方法使用,将上面例子中的allOf.join()方法替换为以下语句。

futureList.forEach(f -> f.exceptionally(e -> {
            allOf.completeExceptionally(e);
            return null;
        }));

//判断allOf即所有任务是否执行完毕
System.out.println(allOf.isDone()); 

运行结果: 截屏2022-01-12 下午7.20.30.png
可以去除任务1的阻塞再执行,结果显示任务1正常执行,任务2异常导致后续任务结束执行。

2.4 扩展

  • JAVA 9对CompletableFuture进行了进一步的升级,可以实现更高阶的功能,因为目前Java 8版本最为普遍,因此不做过多讲述,Java 9 改进的 CompletableFuture API
  • 京东的开源并行框架asyncTools大量使用CompletableFuture,有兴趣可以钻研学习。

三、参考资料