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个方法。
解释:
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的结果是可以正常返回的。
具体解释如下图所示,任务1和任务2是并发执行的,但是获取关系是先后进行,任务1首先执行get(),执行的过程中任务2同时进行,任务2的get()起点是在任务1get()完成点,因此任务2的实际执行时间为任务1执行时间+任务2get时间段。
二、CompletableFuture介绍与用法
2.1 介绍
CompletableFuture是JAVA 8引入的Future的实现类,针对Future做了改进,当异步任务完成或者发生异常时,自动调用回调对象的回调方法,并且提供了函数式编程的能力。CompletableFuture还实现了另外一个接口—CompletionStage,CompletableFuture的众多方法以及函数式能力都是这个接口赋予的。
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使用线程池的用法不做过多展开。
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());
结果为:
例子中让线程阻塞100ms,是为了让程序进行到complete()时,任务还未执行完成,这时complete()的返回值才是true,并且任务判定为完成,可以看到get()的结果为设定的值;如果将sleep去掉,结果为:
可以看到在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()方法,因此后续的方法介绍只选普通情况,也就是使用完成任务的线程来处理后续设定操作的方法。
具体可查看资料: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());
结果:
任务执行遇到异常时,
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());
结果:
如何将组合顺序互换,即将exceptionally()放到下面,结果:
可以看到与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();
运行结果:
如果最后给get()加上限制时间,allOf.get(1000, TimeUnit.MILLISECONDS)。
运行结果:
可以看到某个任务执行超时或抛出异常不影响其他任务的执行。
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();
运行结果:
可以看到,任务2抛出异常后,另外两个任务仍然正常执行完毕。
想要做到任一任务失败整体结束,需要组合exceptionally()方法使用,将上面例子中的allOf.join()方法替换为以下语句。
futureList.forEach(f -> f.exceptionally(e -> {
allOf.completeExceptionally(e);
return null;
}));
//判断allOf即所有任务是否执行完毕
System.out.println(allOf.isDone());
运行结果:
可以去除任务1的阻塞再执行,结果显示任务1正常执行,任务2异常导致后续任务结束执行。
2.4 扩展
- JAVA 9对CompletableFuture进行了进一步的升级,可以实现更高阶的功能,因为目前Java 8版本最为普遍,因此不做过多讲述,Java 9 改进的 CompletableFuture API。
- 京东的开源并行框架asyncTools大量使用CompletableFuture,有兴趣可以钻研学习。