【Java并发·03】CompletableFuture入门
【Java并发·04】CompletableFuture进阶
看完后加网上理解的笔记
初次使用
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。
从Java
8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
俩静态supplyAsync方法,可以有返回结果,而另外俩runAsync是没有返回结果的
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
下面是比对
方法名 | 描述 |
---|---|
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码。 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool执行异步代码。 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值 |
supplyAsync(Supplier<U> supplier, Executor executor) | 使用指定的thread pool执行异步代码,异步操作有返回值 |
所有方法不带Async和带Async区别(重点理解)
async方法始终尝试取新线程执行方法,当然也可能使用当前线程
不带async方法则会从当前线程里取线程执行
前置工具类
一个小工具类打印线程信息和让线程睡眠
public class SmallTool {
public static void sleepMillis(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void printTimeAndThread(String tag) {
String result = new StringJoiner("\t\t")
.add(new SimpleDateFormat("mm分ss秒SSS").format(new Date()))
.add("线程id:" + String.valueOf(Thread.currentThread().getId()))
.add("线程名:" + Thread.currentThread().getName())
.add(tag)
.toString();
System.out.println(result);
}
}
supplyAsync
场景1:
- 小白进入餐厅点菜
- 厨师有炒菜和打饭的过程,都是同一个人完成
- 在这期间小白再打王者
- 做好了开吃
public class _01_supplyAsync {
public static void main(String[] args) {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("厨师炒菜");
SmallTool.sleepMillis(200);
SmallTool.printTimeAndThread("厨师打饭");
SmallTool.sleepMillis(100);
return "番茄炒蛋 + 米饭 做好了";
});
SmallTool.printTimeAndThread("小白在打王者");
//等待结果返回输出
SmallTool.printTimeAndThread(String.format("%s ,小白开吃", cf1.join()));
}
}
47分03秒085 线程id:1 线程名:main 小白进入餐厅
47分03秒086 线程id:1 线程名:main 小白点了 番茄炒蛋 + 一碗米饭
47分03秒150 线程id:1 线程名:main 小白在打王者
47分03秒150 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 厨师炒菜
47分03秒350 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 厨师打饭
47分03秒455 线程id:1 线程名:main 番茄炒蛋 + 米饭 做好了 ,小白开吃
使用supplyAsync
返回结果,内置线程池,最后的语句使用了join
阻塞 等待返结果,不然main线程就直接结束了,看不到打印结果。
CompletableFuture
提供了 join()
方法,它的功能和 get() 方法是一样的,都是阻塞获取值,它们的区别在于 join()
方法抛出的是uncheck异常(即未经检查的异常),不会强制开发者抛出。
thenCompose
public <U> CompletableFuture<U> thenComposeAsync(
Function<? super T, ? extends CompletionStage<U>> fn) {
return uniComposeStage(asyncPool, fn);
}
public interface Function<T, R> {
R apply(T t);
}
允许将两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作,接收Function<入参类型,CompletionStage出参类型>
参数
场景2:
- 小白进入餐厅点菜
- 厨师负责炒菜
- 炒完菜后,服务员才能打饭
- 在这期间小白再打王者
- 做好了开吃
下面的代码意思就是厨师炒完菜后返回结果给服务员,服务员才能去打饭。
public class _02_thenCompose {
public static void main(String[] args) {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("厨师炒菜");
SmallTool.sleepMillis(200);
return "番茄炒蛋";
}).thenCompose(dish -> CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("服务员打饭");
SmallTool.sleepMillis(100);
return dish + " + 米饭";
}));
SmallTool.printTimeAndThread("小白在打王者");
SmallTool.printTimeAndThread(String.format("%s 好了,小白开吃", cf1.join()));
}
/**
* 嵌套调用,也能实现
*/
private static void applyAsync() {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("厨师炒菜");
SmallTool.sleepMillis(200);
CompletableFuture<String> race = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("服务员打饭");
SmallTool.sleepMillis(100);
return " + 米饭";
});
//重点是join()的使用,才能让打饭线程执行完在一起返回
return "番茄炒蛋" + race.join();
});
SmallTool.printTimeAndThread("小白在打王者");
SmallTool.printTimeAndThread(String.format("%s 好了,小白开吃", cf1.join()));
}
}
结果,可以看出
25分14秒988 线程id:1 线程名:main 小白进入餐厅
25分14秒989 线程id:1 线程名:main 小白点了 番茄炒蛋 + 一碗米饭
25分15秒056 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 厨师炒菜
25分15秒057 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 服务员打饭
25分15秒057 线程id:1 线程名:main 小白在打王者
25分15秒061 线程id:1 线程名:main 番茄炒蛋 + 米饭 好了,小白开吃
thenCombine
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn) {
return biApplyStage(null, other, fn);
}
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理,结果由thenCombine 第二个参数BiFunction返回,返回类型如下所示就是R
场景3:
- 小白进入餐厅点菜
- 厨师炒菜,同时服务员蒸饭
- 炒菜蒸饭完成了,服务员才打饭
- 在这期间小白再打王者
- 做好了开吃
代码意思就是把炒菜蒸饭任务结果一起返回,在进行拼接返回。
public class _03_thenCombine {
public static void main(String[] args) {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("厨师炒菜");
SmallTool.sleepMillis(200);
return "番茄炒蛋";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("服务员蒸饭");
SmallTool.sleepMillis(300);
return "米饭";
}), (dish, rice) -> {
SmallTool.printTimeAndThread("服务员打饭");
SmallTool.sleepMillis(100);
return String.format("%s + %s 好了", dish, rice);
});
SmallTool.printTimeAndThread("小白在打王者");
SmallTool.printTimeAndThread(String.format("%s ,小白开吃", cf1.join()));
}
/**
* 用 applyAsync 也能实现
*/
private static void applyAsync() {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("厨师炒菜");
SmallTool.sleepMillis(200);
return "番茄炒蛋";
});
CompletableFuture<String> race = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("服务员蒸饭");
SmallTool.sleepMillis(300);
return "米饭";
});
SmallTool.printTimeAndThread("小白在打王者");
String result = String.format("%s + %s 好了", cf1.join(), race.join());
SmallTool.printTimeAndThread("服务员打饭");
SmallTool.sleepMillis(100);
SmallTool.printTimeAndThread(String.format("%s ,小白开吃", result));
}
}
结果
50分03秒246 线程id:1 线程名:main 小白进入餐厅
50分03秒247 线程id:1 线程名:main 小白点了 番茄炒蛋 + 一碗米饭
56分47秒452 线程id:13 线程名:ForkJoinPool.commonPool-worker-1 厨师炒菜
56分47秒452 线程id:1 线程名:main 小白在打王者
56分47秒454 线程id:14 线程名:ForkJoinPool.commonPool-worker-2 服务员蒸饭
56分47秒754 线程id:14 线程名:ForkJoinPool.commonPool-worker-2 服务员打饭
56分47秒858 线程id:1 线程名:main 番茄炒蛋 + 米饭 好了 ,小白开吃
可以看出厨师炒菜和服务员蒸饭任务合并,两个都完成之后,在执行BiFunction实现类的内容,就是打饭,然后再返回BiFunction的结果
这个是视频里大佬总结的图,很形象
- 第一个在于开启异步任务
- 第二个在于异步任务连接
- 第三个在于异步任务结合
thenApply
把上一个异步执行的结果交给后面的Function,然后返回结果
场景4:
- 小白吃完饭要结账,开发票
- 服务员收款,这个或另外一个服务员开发票
- 回家
下面想要收款和开发票的服务员不同的线程可以用thenApplyAsync
,但是用这个也不一定是新线程,原因最上面说过。
public <U> CompletableFuture<U> thenApplyAsync(
Function<? super T,? extends U> fn) {
return uniApplyStage(asyncPool, fn);
}
public class _01_thenApply {
public static void main(String[] args) {
SmallTool.printTimeAndThread("小白吃好了");
SmallTool.printTimeAndThread("小白 结账、要求开发票");
CompletableFuture<String> invoice = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("服务员收款 500元");
SmallTool.sleepMillis(100);
return "500";
}).thenApply(money -> {
SmallTool.printTimeAndThread(String.format("服务员开发票 面额 %s元", money));
SmallTool.sleepMillis(200);
return String.format("%s元发票", money);
});
SmallTool.printTimeAndThread("小白 接到朋友的电话,想一起打游戏");
SmallTool.printTimeAndThread(String.format("小白拿到%s,准备回家", invoice.join()));
}
}
25分22秒554 线程id:1 线程名:main 小白吃好了
25分22秒556 线程id:1 线程名:main 小白 结账、要求开发票
25分22秒630 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 服务员收款 500元
25分22秒631 线程id:1 线程名:main 小白 接到朋友的电话,想一起打游戏
25分22秒737 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 服务员开发票 面额 500元
25分22秒938 线程id:1 线程名:main 小白拿到500元发票,准备回家
applyToEither
最快返回输出的线程结果作为下一次任务的输入
public <U> CompletableFuture<U> applyToEither(
CompletionStage<? extends T> other, Function<? super T, U> fn) {
return orApplyStage(null, other, fn);
}
场景5:
- 张三吃完饭,出餐厅,到公交等两路公交
- 哪个先到先上哪个
- 上车
下面就是700路和800路哪个先到了,就把结果赋给firstComeBus ,然后返回firstComeBus
public class _02_applyToEither {
public static void main(String[] args) {
SmallTool.printTimeAndThread("张三走出餐厅,来到公交站");
SmallTool.printTimeAndThread("等待 700路 或者 800路 公交到来");
CompletableFuture<String> bus = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("700路公交正在赶来");
SmallTool.sleepMillis(100);
return "700路到了";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("800路公交正在赶来");
SmallTool.sleepMillis(200);
return "800路到了";
}), firstComeBus -> firstComeBus);
SmallTool.printTimeAndThread(String.format("%s,小白坐车回家", bus.join()));
}
}
40分35秒908 线程id:1 线程名:main 张三走出餐厅,来到公交站
40分35秒909 线程id:1 线程名:main 等待 700路 或者 800路 公交到来
40分35秒982 线程id:11 线程名:ForkJoinPool.commonPool-worker-1 700路公交正在赶来
40分35秒983 线程id:12 线程名:ForkJoinPool.commonPool-worker-2 800路公交正在赶来
40分36秒086 线程id:1 线程名:main 700路到了,小白坐车回家
exceptionally
当运行出现异常时,调用该方法可进行一些补偿操作,如设置默认值,就是try catch简化版
场景6:
- 跟场景5一样,不过在做700路时撞树了
- 改叫出租车
public CompletableFuture<T> exceptionally(
Function<Throwable, ? extends T> fn) {
return uniExceptionallyStage(fn);
}
下面代码就是在搭700路时,抛异常,exceptionally输出异常
public class _03_exceptionally {
public static void main(String[] args) {
SmallTool.printTimeAndThread("张三走出餐厅,来到公交站");
SmallTool.printTimeAndThread("等待 700路 或者 800路 公交到来");
CompletableFuture<String> bus = CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("700路公交正在赶来");
SmallTool.sleepMillis(100);
return "700路到了";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("800路公交正在赶来");
SmallTool.sleepMillis(200);
return "800路到了";
}), firstComeBus -> {
SmallTool.printTimeAndThread(firstComeBus);
if (firstComeBus.startsWith("700")) {
throw new RuntimeException("撞树了……");
}
return firstComeBus;
}).exceptionally(e -> {
SmallTool.printTimeAndThread(e.getMessage());
SmallTool.printTimeAndThread("小白叫出租车");
return "出租车 叫到了";
});
SmallTool.printTimeAndThread(String.format("%s,小白坐车回家", bus.join()));
}
}