文章目录
前言
CompletableFuture
到底是什么呢?简单来说,它是一种异步编程工具,可以帮助咱们在未来的某个时刻完成一个计算结果。与Future最大的不同是,它可以被显式地完成,意味着咱们可以在任何时候设置它的值。简单解释如下:
-
异步编程:CompletableFuture允许在后台执行任务,不会阻塞主线程。任务完成后,可以继续执行后续的处理逻辑。
-
链式调用:CompletableFuture支持方法链,可以在任务完成后指定一系列的操作。这些操作可以是同步的,也可以是异步的。
-
组合操作:你可以使用CompletableFuture将多个异步操作组合在一起。例如,可以在一个任务完成后触发另一个任务,或者在多个任务完成后再触发后续操作。
-
异常处理:CompletableFuture提供了对异常的处理能力,可以在异步操作出错时进行异常捕获和处理。
-
简化回调:相比传统的回调机制,CompletableFuture提供了一种更简洁、可读性更好的方式来处理异步任务。
-
并行执行:CompletableFuture能够轻松地将任务并行化,从而提高程序的性能。
一、supplyAsync
作用:开启异步线程且线程具有返回值,方法具有重载可以手动指定线程池或者使用的默认的线程池执行任务
1 supplyAsync(Supplier<U> supplier)
2 supplyAsync(Supplier<U> supplier, Executor executor)
使用默认的线程池
void testSupplyAsync() {
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
// 获取默认线程名字
String defaultThreadName = Thread.currentThread().getName();
System.out.println("默认线程名字:" + defaultThreadName);
return "我被返回啦";
});
System.out.println("supplyAsync返回值:" + supplyAsync.join());
}
输出:
默认线程名字:ForkJoinPool.commonPool-worker-1
supplyAsync返回值:我被返回啦
提示:
join()
用于获取线程执行后返回的结果,有返回值就获取值,没有就是null,后续还会介绍get()
使用指定线程池
区别在于第二个参数是否指定线程池
@Test
void testSupplyAsyncByExecutor() {
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
// 获取指定线程名字
String defaultThreadName = Thread.currentThread().getName();
System.out.println("指定线程池:" + defaultThreadName);
return "我被返回啦";
}, executorService);
System.out.println("supplyAsync返回值:" + supplyAsync.join());
}
输出:
指定线程池:taskExecutor-1
supplyAsync返回值:我被返回啦
二、runAsync
1 runAsync(Runnable runnable, Executor executor)
2 runAsync(Runnable runnable)
和supplyAsync
区别在于runAsync
没有返回值
@Test
void testRunAsync() {
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
System.out.println("runAsync没有返回值");
}, executorService);
}
输出:
runAsync没有返回值
三、thenAccept & thenAcceptAsync
thenAccept
thenAccept
等待上一次线程执行完后,获取结果执行下一步操作
下面代码表示执行完task1
后获取task1的返回值作
为形参传入给thenAccept
如果不是很明白可以反复看看,后续thenRun,thenApply的逻辑和这个是类似的,区别只在参数和返回值上
@Test
void testThenAccept() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<Void> task2 = task1.thenAccept(s -> {
System.out.println(s + "task2");
});
}
输出:
task1task2
thenAcceptAsync
thenAcceptAsync
支持自己指定线程池,如果不指定则使用默认的线程池ForkJoinPool
public <U> CompletableFuture<U> thenApplyAsync(
Function<? super T,? extends U> fn) {
return uniApplyStage(defaultExecutor(), fn);
}
defaultExecutor()
public Executor defaultExecutor() {
return ASYNC_POOL;
}
ASYNC_POOL
private static final Executor ASYNC_POOL = USE_COMMON_POOL ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
thenAccept
和thenAcceptAsync
的区别:
- thenAccept不能指定线程池,thenAcceptAsync可以指定或者使用默认的线程池
- thenAccept 默认沿用上一次使用的线程池中的线程
- thenAcceptAsync没有指定线程池会沿用上一次使用的线程池,如果指定了就使用指定的线程池执行
thenAccept
使用上一次结果的线程池
@Test
void testThenAccept() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<Void> task2 = task1.thenAccept(s -> {
System.out.println(Thread.currentThread().getName());
});
}
输出:
ForkJoinPool.commonPool-worker-1
@Test
void testThenAccept() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
}, executorService);
CompletableFuture<Void> task2 = task1.thenAccept(s -> {
System.out.println(Thread.currentThread().getName());
});
}
输出:
taskExecutor-1
区别在于:第一个代码supplyAsync
没有指定线程池,使用了默认的ForkJoinPool
,所以thenAccept
也会沿用这个线线程池;代码2使用了自定义的线程池,thenAccept
沿用,输出的是自定义线程池的名称
thenAcceptAsync
使用默认的线程池
@Test
void testThenApplyAsync() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
}, executorService);
CompletableFuture<Void> task2 = task1.thenAcceptAsync(s -> {
System.out.println(Thread.currentThread().getName());
});
}
输出:
ForkJoinPool.commonPool-worker-1
虽然说then
开头的会沿用上一次使用的线程池,上面代码中supplyAsync
指定了executorService
这个线程池,但是thenAcceptAsync
还是使用的默认线程池,所以带Async
的方法,跟上一次使用的是什么线程是没关系的,如果不指定就是默认的,如果指定了就是指定线程
thenAcceptAsync指定线程池
@Test
void testThenApplyAsync() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<Void> task2 = task1.thenAcceptAsync(s -> {
System.out.println(Thread.currentThread().getName());
}, executorService);
}
输出:
taskExecutor-1
总结
以then
开头的都会沿用上一次使用的线程池且没法指定线程池,但是如果是Async
结尾会使用默认的ForkJoinPool
线程池,如果指定了就使用指定的线程池(例如supplyAsync,thenAcceptAsync等等)
- then*沿用上一次线程,不能自己指定
- then*Asnyc使用默认的ForkJoinPool线程池,指定了就是使用指定的
还有很多以Async
结尾的方法,他们和没有以Async
结尾的方法都是这个区别,可以体会一下
四、thenApply & thenApplyAsync
thenApply
和 thenAccept
区别在于apply不仅获取上一次结果作为参数,还具有返回值
@Test
void testThenApply() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<String> task2 = task1.thenApply(s -> {
return s + "task2";
});
System.out.println(task2.join());
}
输出:
task1task2
thenApply
和thenApplyAsync
区别在于Async
,不清楚可以再看看第三点
五、thenRun & thenRunAsync
thenRun
既没有返回值也获取不到上一次的结果
@Test
void testThenRun() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<Void> task2 = task1.thenRun(() -> {
System.out.println("不能获取上一次执行的结果");
});
}
输出:
不能获取上一次执行的结果
thenRun
和thenRunAsync
区别在于Async
,不清楚可以再看看第三点
总结
thenRun
既没有参数也没有返回值thenAccept
有参没有返回值thenApply
有参有返回值
参数:上一次执行的结果作为参数
六、thenCompose
thenCompose可以接受一个参数并且包含返回值,是不是和thenApply一模一样,区别在于thenCompose的返回值必须是? extends CompletionStage<U>> fn
这么说你可能不认识,但是通过这个public class CompletableFuture<T> implements Future<T>, CompletionStage<T>
能看出CompletableFuture是实现了CompletionStage
这个接口的,感兴趣可以直接点进去看看。总结来说thenCompose的返回值必须是
一个CompletableFuture类型的,和thenApply区别在于,thenApply可以返回任意类型
@Test
void testThenCompose() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<String> thenCompose = task1.thenCompose((s) -> {
System.out.println(Thread.currentThread().getName()); // 1
return CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName()); // 2
return s + "task2";
});
});
System.out.println(thenCompose.join()); // 3
}
上述代码3处输出:
task1task2
提问:对于上述代码1
、2
处分别输出什么?
如果一时回答不上来,可以考虑从第三点重新阅读一下,加深理解。可以运行一下看看是否和自己想的一样
如果代码如下:1、2处又分别输出什么呢?
@Test
void testThenCompose() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
}, executorService);
CompletableFuture<String> thenCompose = task1.thenCompose((s) -> {
System.out.println(Thread.currentThread().getName()); // 1
return CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName()); // 2
return s + "task2";
});
});
System.out.println(thenCompose.join());
}
错误例子:
@Test
void testThenCompose() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
task1.thenCompose((s) -> {
return "测试";
});
}
错误例子中展示的是thenCompose返回一个字符串类型,前面说过thenCompose只能返回CompletableFuture类型
,所以上述代码会报错
七、thenCombine
thenCombine( CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) {
用于组合两次CompletableFuture,等到他们都完成了才会执行后续逻辑,thenCombine第一个参数为需要进行组合的CompletableFuture,此处表示task1和task2进行组合
,第二个参数为task1执行的结果和task2执行的结果
@Test
void testThenCombine() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
}, executorService);
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
return "task2";
});
CompletableFuture<String> thenCombine = task1.thenCombine(task2, (t1, t2) -> {
System.out.println(Thread.currentThread().getName()); // 1
return t1 + t2;
});
System.out.println(thenCombine.join()); // 2
}
输出:
task1task2
提问:1处输出什么呢?
如果你说是executorService这个自定义线程池中线程名字,说明你对then已经有了一个深刻的理解。但是很抱歉告诉你回答错误。1处输出的是main这个线程。不要惊讶,很快就让你理解!首先理解thenCombine的作用是不是等待task1和task2完成之后执行方法体里面的内容呢?thenCombine不支持参数传入自定义线程池,按照之前的理解会默认使用task1的线程池中的线程。
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn) {
return biApplyStage(null, other, fn);
}
biApplyStage
方法第一个参数为线程池(此处为null),我们继续看biApplyStage()这个方法,省略部分代码,感兴趣可以自行查看。下面代码1处可以看到接受一个Executor
,但是上面方法中传入过来的是一个null,说明没有指定Executor,代码中也没有默认的线程。判断r、s是否执行完毕,此处的r、s表示task1和task2这两个任务,如果两个都执行完成走3处,调用biApply()
,如果没有完成就走2处,执行bipush(b, new BiApply<T,U,V>(e, d, this, b, f))
。简单理解主线程执行到combine
的时候两个任务都完成了,那么此时执行3处代码,表示使用当前线程执行后续逻辑,也就是提问1中为什么输出的是main;如果主线程执行到combine任意一个线程没有执行完,那么就会走2处的逻辑。2处方法内部就描述了使用哪一个线程执行任务
这可以提高性能,避免了上下文切换和线程创建的开销。
private <U,V> CompletableFuture<V> biApplyStage(
Executor e, CompletionStage<U> o, // 1
BiFunction<? super T,? super U,? extends V> f) {
// ...省略...
if ((r = result) == null || (s = b.result) == null)
bipush(b, new BiApply<T,U,V>(e, d, this, b, f)); // 2
else if (e == null)
d.biApply(r, s, f, null); // 3
else
try {
e.execute(new BiApply<T,U,V>(null, d, this, b, f));
} catch (Throwable ex) {
d.result = encodeThrowable(ex);
}
return d;
}
2处父类核心方法,可以自行查阅。
在 claim 方法中,如果 executor 为 null,那么它会尝试直接运行任务。
如果 executor 不为 null,那么它会使用这个 executor 来执行任务。
final boolean claim() {
Executor e = executor;
if (compareAndSetForkJoinTaskTag((short)0, (short)1)) {
if (e == null)
return true;
executor = null; // disable
e.execute(this);
}
return false;
}
所以对于thenCombine是主线程还是别的线程执行,取决于依赖的任务是否完成
提问2:下面代码1
处输出的线程是什么?
@Test
void testThenCombine() throws InterruptedException {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
}, executorService);
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "task2";
});
CompletableFuture<String> thenCombine = task1.thenCombine(task2, (t1, t2) -> {
System.out.println(Thread.currentThread().getName()); // 1
return t1 + t2;
});
System.out.println(thenCombine.join());
}
答案:
ForkJoinPool.commonPool-worker-1
解释:主线程执行thenCombine
这个方法的时候,判断task1
和task2
是否完成,显然task2
没有完成(因为还在休眠),所以thenCombine会继续使用task2的线程池来执行方法
。 到可以反复理解一下,手动运行一下代码,尝试把task1睡眠2s尝试一下输出1处的线程是什么。
总结
主线程执行到combine的时候,判断线程完成情况来选择由哪个线程执行thenCombine
情况1
:都完成,主线程执行thenCombine情况2
:任意线程未完成,最后完成的线程执行thenCombine
提问3: thenCombine
和thenCombineAsync
的区别是什么?如果不清楚,欢迎继续从第三点开始阅读。
八、runAfterEither & runAfterBoth
runAfterEither
表示两个任务任何一个完成都会执行后续方法。runAfterEither
线程沿用规则:主线程执行到runAfterEither()
的时候任意一个方法执行完了,此时线程为main
;执行到runAfterEither
,没有一个线程执行完,那么就等最谁先执行完,最先完成的线程继续执行runAfterEither
。可以自行对task1或者2进行休眠打印runAfterEither中的线程。
@Test
void testRunAfterEither() throws InterruptedException {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
return "task2";
});
CompletableFuture<Void> runAfterEither= task1.runAfterEither(task2, () -> {
System.out.println("任何一个完成即可");
});
}
输出:
任何一个完成即可
runAfterBoth
必须要两个方法都执行完才会执行后续方法。线程沿用规则同runAfterEither
,区别在于执行到runAfterBoth时,需要等到两个线程都执行完。最后执行完的线程继续执行runAfterBoth
。可以自行测试一下,加深理解。
@Test
void testRunAfterEither() throws InterruptedException {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
return "task2";
});
CompletableFuture<Void> runAfterBoth = task1.runAfterBoth(task2, () -> {
System.out.println("两个都要完成才行");
});
runAfterBoth.join();
}
输出
两个都要完成才行
九、whenComplete & handle
whenComplete
,handle
接受两个参数,第一个是任务执行成功的结果,第二个是异常信息。同时处理正常的结果和异常情况。
@Test
void testWhenComplete() {
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
int a = 1 / 0;
return "task1";
}, executorService);
CompletableFuture<String> whenComplete = task.whenComplete((success, error) -> {
System.out.println(success);
if (error != null) System.out.println(error.getMessage());
});
}
输出:
null
java.lang.ArithmeticException: / by zero
handle
和 whenComplete
区别在于前者具有返回值,后者没有返回值。
@Test
void testHandle() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
return "task1";
});
CompletableFuture<String> handle = task1.handle((success, error) -> {
if (error != null) {
return "error";
}
return success;
});
System.out.println(handle.join());
}
输出:
task1
十、exceptionally & exceptionallyCompose
exceptionally
只当任务发成错误才会执行方法,类似与try/catch用于捕获错误
,一个参数,一个返回值。参数为Throwable
,返回值任意
, 没有错误exceptionally
是不会有任何内容的。
@Test
void testExceptionally() {
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
int a = 1 / 0;
return "task1";
});
CompletableFuture<String> exceptionally = task.exceptionally((error) -> {
return error.getMessage();
});
System.out.println(exceptionally.join());
}
输出
java.lang.ArithmeticException: / by zero
exceptionallyCompose
区别在于返回值是一个CompletableFuture
类型。
十一、allOf & anyOf
allOf
表示等待所有任务都执行完
@Test
void testAllOf() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "task1");
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "task2");
CompletableFuture<String> task3 = CompletableFuture.supplyAsync(() -> "task3");
CompletableFuture<Void> allOf = CompletableFuture.allOf(task1, task2, task3);
allOf.join();
System.out.println("所有任务都执行完了");
}
andOf
表示任意一个任务执行完即可。输出为最先完成的任务的返回值
@Test
void testAnyOf() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "task1");
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "task2");
CompletableFuture<String> task3 = CompletableFuture.supplyAsync(() -> "task3");
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(task1, task2, task3);
System.out.println(anyOf.join());
}
输出:
task1
十二、cancel & complete
cancel
取消任务,取消任务之后,后续无法继续获取该任务,但是没法立即中断已经执行的任务。 代码如下,task1异步线程开启之后,主线程取消任务,但是task1会继续执行,redis数据添加成功。取消仅仅是后续没法获取已经取消的任务。cancel有两个参数: true
/false
,true表示立即去尝试中断正在执行的任务,false表示不立即中断正在执行的任务(至于是否中断成功,那就不知道了)。true/false都会标记任务是已取消的状态。
@Test
void testCancel() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stringRedisTemplate.opsForValue().set("key", "测试取消");
return "task1";
});
task1.join();
task1.cancel(true);
System.out.println(task1.join());
}
complete
使任务直接完成,当 supplyAsync 方法被调用时,异步任务会在另一个线程上开始执行。调用 task1.complete(“直接完成”) 会立即完成 task1 并设置结果为 “直接完成”。这意味着 task1 不再等待异步任务的结果。即使 task1 已经被标记为完成,异步任务仍然会在另一个线程上继续执行,但它不会影响 task1 的最终结果
@Test
void testComplete() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "task1";
});
task1.complete("直接完成");
System.out.println(task1.join());
}
输出:
直接完成
额外补充
1 CompletionException
有一点需要注意,CompletableFuture
在回调方法中对异常进行了包装。大部分异常会封装成CompletionException
后抛出,真正的异常存储在cause
属性中,因此如果调用链中经过了回调方法处理那么就需要用Throwable.getCause()
方法提取真正的异常。但是,有些情况下会直接返回真正的异常,所以最好使用工具类提取异常
,如下代码所示:
@Test
void testException2() {
CompletableFuture<Void> root = new CompletableFuture<>();
CompletableFuture<Void> child = root.whenComplete((v, t) -> {
System.out.println(t.getClass()); // 1 class java.io.Exception
});
child.whenComplete((v, t) -> {
System.out.println(t.getClass()); // 2 class java.util.concurrent.CompletionException
});
root.completeExceptionally(new IOException()); // 3
}
输出:
class java.io.IOException
class java.util.concurrent.CompletionException
completeExceptionally用法类似于complete,complete表示正常完成任务,completeExceptionally表示异常完成任务(具体概念可以参考complete)。上述代码中主线程3
处抛出了一个IO
异常,代码1
处打印出了这个IO
异常,此时是没有包装的真正异常,但是链式调用child
输出的异常类型却是 CompletionException
,这是对IO
异常进行了一个包装,所以2
处输出的异常类型我们根本不知道是什么异常
。只能通过Throwable.getCause()
获取。代码如下(仅修改2处代码):
@Test
void testException2() {
CompletableFuture<Void> root = new CompletableFuture<>();
CompletableFuture<Void> child = root.whenComplete((v, t) -> {
System.out.println(t.getClass()); // 1 class java.io.Exception
});
child.whenComplete((v, t) -> {
System.out.println(t.getCause().getClass()); // 2 class java.io.IOException
});
root.completeExceptionally(new IOException());
}
输出:
class java.io.IOException
class java.io.IOException
上述代码发现输出了正确的异常,但是如果每个包装后的CompletionException异常都要手动输出实际异常稍显麻烦,最好使用工具类提取异常,如下代码所示:
@Test
void testException2() {
CompletableFuture<Void> root = new CompletableFuture<>();
CompletableFuture<Void> child = root.whenComplete((v, t) -> {
System.out.println(ExceptionUtils.extractRealException(t).toString()); // 1 class java.io.Exception
});
child.whenComplete((v, t) -> {
System.out.println(ExceptionUtils.extractRealException(t).toString()); // 2 class java.io.IOException
});
root.completeExceptionally(new IOException());
}
ExceptionUtils
工具类
public class ExceptionUtils {
public static Throwable extractRealException(Throwable throwable) {
//这里判断异常类型是否为CompletionException、ExecutionException,如果是则进行提取,否则直接返回。
if (throwable instanceof CompletionException || throwable instanceof ExecutionException) {
if (throwable.getCause() != null) {
return throwable.getCause();
}
}
return throwable;
}
}
2 代码执行在哪个线程上
要合理治理线程资源,最基本的前提条件就是要在写代码时,清楚地知道每一行代码都将执行在哪个线程上。前文已经阐述很多这里做个小总结:
CompletableFuture实现了CompletionStage接口,通过丰富的回调方法,支持各种组合操作,每种组合场景都有同步和异步两种方法。
1 同步方法(即不带Async
后缀的方法)有两种情况。
- 如果注册时被依赖的操作已经执行完成,则直接由当前线程执行。
- 如果注册时被依赖的操作还未执行完,则由回调线程执行。
2 异步方法(即带Async
后缀的方法):可以选择是否传递线程池参数Executor
运行在指定线程池中;当不传递Executor时,会使用ForkJoinPool中的共用线程池CommonPool(CommonPool的大小是CPU核数-1,如果是IO密集的应用,线程数可能成为瓶颈)。
3 线程池须知
前面提到,异步回调方法可以选择是否传递线程池参数Executor,这里建议强制传线程池
,且根据实际情况做线程池隔离。
当不传递线程池时,会使用ForkJoinPool中
的公共线程池CommonPool
,这里所有调用将共用该线程池,核心线程数=处理器数量-1(单核核心线程数为1)
,所有异步回调都会共用该CommonPool
,核心与非核心业务都竞争同一个池中的线程,很容易成为系统瓶颈
。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的相互干扰。
感谢阅读,欢迎提出问题!
参考文献
链接: docs.oracle/CompletableFuture
链接: CompletableFuture原理与实践-外卖商家端API的异步化
链接: stackoverflow/does-completionstage-always-wrap-exceptions-in-completionexception