前面一个篇我们降到FutureTask,从调用事例中说到了FutureTask的缺点,get()方法在计算完成之前一直处于阻塞状态,isDone()方法容易耗费CPU资源。对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动回调该函数,这样我们就不用等待结果。CompletableFuture提供了一种观察模式类似的机制,可以让任务执行完成后通知监听的一方。
CompletableFuture
如下图,CompletableFuture实现了Future接口和CompletionStage接口。也就是说Future所拥有的功能特性CompletableFuture也都具备,Future接口没有的CompletableFuture通过实现CompletionStage接口进一步得到加强。
CompletionStage
CompletionStage
接口提供了一种编排异步任务的方式,使得我们可以以链式的形式组合和处理多个异步任务。
在使用 CompletionStage
编排异步任务时,我们可以通过以下方法来组合和处理任务:
-
thenApply(Function<? super T,? extends U> fn)
:将当前任务的结果应用给定的函数转换为新的结果,并返回一个新的CompletionStage
对象。 -
thenAccept(Consumer<? super T> action)
:对当前任务的结果执行给定的操作,不返回任何结果。 -
thenRun(Runnable action)
:在当前任务完成后执行给定的动作,不依赖任务的结果。 -
thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
:将当前任务的结果传递给给定的函数,生成一个新的CompletionStage
对象。这个新的CompletionStage
对象表示了对当前任务结果的进一步操作。 -
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
:组合当前任务与另一个任务的结果,然后将它们传递给给定的函数进行处理,并返回一个新的CompletionStage
对象。 -
exceptionally(Function<Throwable, ? extends T> fn)
:处理当前任务过程中抛出的异常。如果当前任务正常完成,则返回当前任务的结果;如果任务抛出异常,则会调用给定的函数来处理异常并返回一个新的CompletionStage
对象。 -
handle(BiFunction<? super T, Throwable, ? extends U> fn)
:对当前任务的结果或可能抛出的异常进行处理,返回一个新的CompletionStage
对象。
这些方法可以在 CompletionStage
之间以链式的方式调用,形成一个异步任务流水线。每个方法都会返回一个新的 CompletionStage
对象,表示了对前一个阶段的处理结果。这样,我们可以将多个异步任务有序地连接起来,而无需显式地等待每个任务的完成。通过使用适当的组合和处理方法,我们可以编排复杂的异步任务流程,实现更高效的异步编程,并且能够更好地控制任务之间的依赖关系、并行度和错误处理。
CompletableFuture使用案例
开始案例之前,先介绍一个简单的工具类。主要是用来打印当前线程相关的信息的,如下:
public class TlhTool {
/**
* 当前线程睡眠指定时间
*
* @param seconds 指定时间(单位:秒)
*/
public static void sleep(Integer seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 打印出当前时间、线程id、线程名字
*
* @param tag 指定tag
*/
public static void printTimeAndThread(String tag) {
String result = new StringJoiner("\t|\t")
.add(String.valueOf(System.currentTimeMillis()))
.add(String.valueOf(Thread.currentThread().getId()))
.add(Thread.currentThread().getName())
.add(tag)
.toString();
System.out.println(result);
}
}
案例一
小白去餐厅吃饭,小白一到餐厅点了一份西红柿炒鸡蛋,小白就玩起了王者,一边玩游戏一边等西红柿炒鸡蛋。厨师就去给小白炒西红柿炒鸡蛋然和给小白打饭。
代码如下:
public class _01_supplyAsync {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白进入餐厅");
TlhTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("厨师炒菜");
TlhTool.sleep(2);
TlhTool.printTimeAndThread("厨师打饭");
TlhTool.sleep(1);
return "番茄炒蛋 + 米饭 做好了";
});
TlhTool.printTimeAndThread("小白在打王者");
//join方法会等待任务执行结束,然后返回任务的结果
TlhTool.printTimeAndThread(String.format("%s ,小白开吃", cf1.join()));
}
}
运行结果:
1690211623592 | 1 | main | 小白进入餐厅
1690211623592 | 1 | main | 小白点了 番茄炒蛋 + 一碗米饭
1690211623633 | 1 | main | 小白在打王者
1690211623633 | 12 | ForkJoinPool.commonPool-worker-9 | 厨师炒菜
1690211625646 | 12 | ForkJoinPool.commonPool-worker-9 | 厨师打饭
1690211626674 | 1 | main | 番茄炒蛋 + 米饭 做好了 ,小白开吃
案例中,CompletableFuture通过静态方法supplyAsync()接受一个Supplier对象开启一个异步任务,返回一个CompletableFuture对象。最后通CompletableFuture的join()方法获得异步任务的结果。
案例二
小白去餐厅吃饭,小白一到餐厅点了一份西红柿炒鸡蛋,小白就玩起了王者,一边玩游戏一边等西红柿炒鸡蛋。这个时候炒西红柿炒鸡蛋的依然是厨师,但是打饭的却是服务员。服务员等厨师炒好西红柿炒鸡蛋之后连同饭和西红柿炒鸡蛋一并端给小白。
代码如下:
public class _02_thenCompose {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白进入餐厅");
TlhTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("厨师炒菜");
TlhTool.sleep(2);
return "番茄炒蛋";
}).thenCompose(dish -> CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("服务员打饭");
TlhTool.sleep(1);
return dish + " + 米饭";
}));
TlhTool.printTimeAndThread("小白打王者");
System.out.println(String.format("%s ,小白开吃", cf1.join()));
}
}
打印结果:
1690212383636 | 1 | main | 小白进入餐厅
1690212383637 | 1 | main | 小白点了 番茄炒蛋 + 一碗米饭
1690212383682 | 12 | ForkJoinPool.commonPool-worker-9 | 厨师炒菜
1690212383682 | 1 | main | 小白打王者
1690212385686 | 12 | ForkJoinPool.commonPool-worker-9 | 服务员打饭
1690212386717 | 1 | main | 番茄炒蛋 + 米饭 ,小白开吃
案例中,小白依然是一边玩王者一边等自己的西红柿炒鸡蛋和米饭。但是现在炒菜和打饭不是同一个人,而且打饭的人还需要等另一个人炒完菜一起端给小白,也就是说第二个异步任务依赖于第一个异步任务的结果。我们使用thenCompose()方法取得上一个异步任务的结果做进一步的处理,返回一个CompletableFuture对象,在通过其join()方法获得最终结果。
案例三
小白去餐厅吃饭,小白一到餐厅点了一份西红柿炒鸡蛋,小白就玩起了王者,一边玩游戏一边等西红柿炒鸡蛋。这个时候炒西红柿炒鸡蛋的依然是厨师,但是更加糟糕的情况是饭店的饭刚刚被吃完了需要重新煮,等饭熟了之后服务员才能拿着厨师炒好的西红柿炒鸡蛋和米饭一起端给小白。
代码如下:
public class _03_thenCombine {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白进入餐厅");
TlhTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("厨师炒菜");
TlhTool.sleep(2);
return "番茄炒蛋";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("服务员蒸饭");
TlhTool.sleep(3);
return "米饭";
}), (dish, rice) -> {
TlhTool.printTimeAndThread("服务员打饭");
TlhTool.sleep(1);
return String.format("%s + %s", dish, rice);
});
TlhTool.printTimeAndThread("小白在打王者");
System.out.println(String.format("%s ,小白开吃", cf1.join()));
}
}
打印结果:
1690213042443 | 1 | main | 小白进入餐厅
1690213042443 | 1 | main | 小白点了 番茄炒蛋 + 一碗米饭
1690213042480 | 12 | ForkJoinPool.commonPool-worker-9 | 厨师炒菜
1690213042480 | 13 | ForkJoinPool.commonPool-worker-2 | 服务员蒸饭
1690213042480 | 1 | main | 小白在打王者
1690213045482 | 13 | ForkJoinPool.commonPool-worker-2 | 服务员打饭
1690213046509 | 1 | main | 番茄炒蛋 + 米饭 ,小白开吃
案例中,服务员需要等厨师炒完西红柿炒鸡蛋和饭蒸熟才能端给小白,也就是说服务员打饭依赖于厨师炒好菜和饭被蒸熟这两个结果。我们使用thenCombine()方法将两个异步任务连接起来,然后在处理两个异步任务的结果。thenCombine()方法接受一个CompletionStage和一个BiFunction,这里的CompletionStage就可以理解成一个CompletableFuture,而BiFunction是一个函数式接口,会将两个异步任务的返回拿到交给改接口的实现来处理。
案例四
经过案例一、案例二、案例三的折腾,小白终于完了自己的西红柿炒鸡蛋。小白吃完了西红柿炒鸡蛋结账完成后需要开发票,这个时候饭店结账的服务员结完账之后把收据给到财务小姐姐开票。
代码如下:
public class _04_thenApply {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白吃好了");
TlhTool.printTimeAndThread("小白结账,要求开发票");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("服务员收款500元");
TlhTool.sleep(1);
return "500";
}).thenApplyAsync((money) -> {
TlhTool.printTimeAndThread(String.format("财务开发票,面额为%s元", money));
TlhTool.sleep(1);
return String.format("%s元发票", money);
});
TlhTool.printTimeAndThread("小白接到朋友的电话,想一起打游戏");
TlhTool.printTimeAndThread(String.format("小白拿到%s,准备回家", cf1.join()));
}
}
打印如下:
1690213966283 | 1 | main | 小白吃好了
1690213966283 | 1 | main | 小白结账,要求开发票
1690213966317 | 12 | ForkJoinPool.commonPool-worker-9 | 服务员收款500元
1690213966318 | 1 | main | 小白接到朋友的电话,想一起打游戏
1690213967351 | 12 | ForkJoinPool.commonPool-worker-9 | 财务开发票,面额为500元
1690213968365 | 1 | main | 小白拿到500元发票,准备回家
看到这里,有的同学就问了这里为啥使用thenApplyAsync()方法,案例二中的thenCompose()方法不是可以完成吗?是的,使用thenCompose()方法也能完成改案例。但是两个方法的使用场景有那么一丢丢不一样,thenCompose()方法是一个CompletableFuture依赖于另一个CompletableFuture的结果,而thenApplyAsync()方法则是直接处理异步任务的结果。
案例五
小白吃完饭了,接到朋友的电话想和他一起玩游戏。小白走出餐厅,这个时候小白有两种方案回家:要莫坐145路公交回家,要莫坐116路公交回家。小白决定那路公交先来小白就坐那路回家。
代码如下:
public class _05_applyToEither {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白走出餐厅");
TlhTool.printTimeAndThread("等待 145路 公交或者 116路 公交");
CompletableFuture<String> bus = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("145路公交正在赶来");
TlhTool.sleep(5);
return "145路公交到了";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("116路公交正在赶来");
TlhTool.sleep(2);
return "116路公交到了";
}), firstComBus -> firstComBus);
TlhTool.printTimeAndThread(String.format("%s,小白坐车回家了", bus.join()));
}
}
打印结果:
1690214625599 | 1 | main | 小白走出餐厅
1690214625599 | 1 | main | 等待 145路 公交或者 116路 公交
1690214625633 | 12 | ForkJoinPool.commonPool-worker-9 | 145路公交正在赶来
1690214625633 | 13 | ForkJoinPool.commonPool-worker-2 | 116路公交正在赶来
1690214627659 | 1 | main | 116路公交到了,小白坐车回家了
案例中,使用线程sleep的方式模拟公交到站的耗时,可以看到116路公交耗时2秒就到了公交站早于145路公交。所以小白就坐116路公交回家了。applyToEither()方法接受一个CompletableFuture对象和Function对象,哪一个任务先完成Function对象就会接受到对应异步任务的结果,返回一个CompletableFuture对象,通过其join()方法就能拿到最终结果。
案例六
小白坐上116公交车,屋漏偏逢连夜雨,哐当...116路公交撞树上了。小白为了和朋友一起打游戏,于是打上了出租车回家。
代码如下:
public class _06_exceptionally {
public static void main(String[] args) {
TlhTool.printTimeAndThread("小白走出餐厅");
TlhTool.printTimeAndThread("等待 145路 公交或者 116路 公交");
CompletableFuture<String> bus = CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("145路公交正在赶来");
TlhTool.sleep(5);
return "145路公交到了";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
TlhTool.printTimeAndThread("116路公交正在赶来");
TlhTool.sleep(2);
return "116路公交到了";
}), firstComBus -> {
TlhTool.printTimeAndThread(firstComBus);
if (firstComBus.startsWith("116")) {
throw new RuntimeException("撞树上了....");
}
return firstComBus;
}).exceptionally(e -> {
TlhTool.printTimeAndThread(e.getMessage());
TlhTool.printTimeAndThread("小白叫 出租车");
TlhTool.sleep(3);
return "出租车 叫到了";
});
TlhTool.printTimeAndThread(String.format("%s,小白坐车回家了", bus.join()));
}
}
打印结果:
1690215058232 | 1 | main | 小白走出餐厅
1690215058232 | 1 | main | 等待 145路 公交或者 116路 公交
1690215058267 | 12 | ForkJoinPool.commonPool-worker-9 | 145路公交正在赶来
1690215058267 | 13 | ForkJoinPool.commonPool-worker-2 | 116路公交正在赶来
1690215060276 | 13 | ForkJoinPool.commonPool-worker-2 | 116路公交到了
1690215060276 | 13 | ForkJoinPool.commonPool-worker-2 | java.lang.RuntimeException: 撞树上了....
1690215060276 | 13 | ForkJoinPool.commonPool-worker-2 | 小白叫 出租车
1690215063309 | 1 | main | 出租车 叫到了,小白坐车回家了
该案例中,突出的就是exceptionally能够捕获CompletableFuture中抛出的异常,如果CompletableFuture没有抛异常的话就不会跑这块代码逻辑。最后小白终于可以和朋友愉快的玩游戏了。