4. CompletableFuture
4.1 Future 和 Callable 接口
Future
接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
Callable
接口中定义了需要有返回的任务需要实现的方法。
使用途径:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果。
4.2 FutureTask
是什么:未来的任务,用它就干一件事,异步调用main方法。就像一个冰糖葫芦,一个个方法由main串起来。但解决不了一个问题:正常调用挂起堵塞问题
例子:
(1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水,水买回来了放桌上,我需要的时候再去get。
(2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果
(3)高考:会做的先做,不会的放在后面做
原理:在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成。当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态( get 方法)。一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。 只计算一次,get 方法一般放到最后。
FutureTask 类的关系图
get() 阻塞
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
System.out.println("-----come in FutureTask");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextInt(100);
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
// 3秒钟后才出来结果,还没有计算你提前来拿(只要一调用get方法,对于结果就是不见不散,会导致阻塞)
// System.out.println(Thread.currentThread().getName() + "\t" + futureTask.get());
// 3秒钟后才出来结果,我只想等待1秒钟,过时不候 然后报 TimeoutException
// System.out.println(Thread.currentThread().getName() + "\t" + futureTask.get(1, TimeUnit.SECONDS));
System.out.println(Thread.currentThread().getName() + "\t" + " run... here");
}
一旦调用get()方法,不管是否计算完成都会导致阻塞
isDone() 轮询
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println("-----come in FutureTask");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "" + ThreadLocalRandom.current().nextInt(100);
});
new Thread(futureTask, "t1").start();
// 用于阻塞式获取结果,如果想要异步获取结果,通常都会以轮询的方式去获取结果
while (true) {
if (futureTask.isDone()) {
System.out.println("计算完毕,结果为:" + futureTask.get());
break;
} else {
System.out.println("还在计算中");
}
}
System.out.println(Thread.currentThread().getName() + "\t" + " run... here");
}
轮询的方式会耗费无谓的 CPU 资源,而且也不见得能及时地得到计算结果。
如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞。
但是我们想要完成一些复杂的任务,如下:
- 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
- 将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果
- 当Future集合中某个任务最快结束时,返回结果
- 等待Future结合中的所有任务都完成
- …
对于上述的任务,当我们继续使用 futureTask 时就会显得很累赘,而且还会阻塞,这时候我们就要考虑采用新的技术。
4.3 对Future的改进
4.3.1 CompletableFuture 和 CompletionStage 介绍
类架构说明
接口 CompletionStage
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
类 CompletableFuture
4.3.2 核心的四个静态方法,来创建一个异步操作
runAsync
无返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
supplyAsync
有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
Executor executor
参数说明:
- 没有指定 Executor 的方法,直接使用默认的
ForkJoinPool.commonPool()
作为它的线程池执行异步代码 - 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
代码:
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 2, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
System.out.println("-----task is over");
});
System.out.println(future1.get());
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
System.out.println("-----task is over");
}, executor);
System.out.println(future2.get());
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
System.out.println("-----task is over");
return 11;
});
System.out.println(future3.get());
CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
System.out.println("-----task is over");
return 11;
}, executor);
System.out.println(future4.get());
executor.shutdown();
}
// 结果------使用 get 方法仍然会阻塞,因为仍然可以使用 Future 接口中的方法
ForkJoinPool.commonPool-worker-1 -----come in
-----task is over
null
pool-1-thread-1 -----come in
-----task is over
null
ForkJoinPool.commonPool-worker-1 -----come in
-----task is over
11
pool-1-thread-1 -----come in
-----task is over
11
常用 Code 演示,减少阻塞和轮询:
从 Java8 开始引入了 CompletableFuture
,它是 Future
的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
int result = ThreadLocalRandom.current().nextInt(10);
// 暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----计算结束耗时1秒钟,result: " + result);
if (result > 6) {
int age = 10 / 0;
}
return result;
}, executor).thenApply(f -> {
System.out.println("--------------继续计算-----------,进行 + 2 操作");
return f + 2;
}).whenComplete((r, e) -> {
if (e == null) {
System.out.println("-----result: " + r);
}
}).exceptionally(e -> {
System.out.println("-----exception: " + e.getCause() + "\t" + e.getMessage());
return -1;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(future.join());
System.out.println("----------------main over");
executor.shutdown();
}
// 结果之一:
pool-1-thread-1 -----come in
-----计算结束耗时1秒钟,result: 3
--------------继续计算-----------,进行 + 2 操作
-----result: 5
5
----------------main over
// 结果之二:
pool-1-thread-1 -----come in
-----计算结束耗时1秒钟,result: 8
-----exception: java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
-1
----------------main over
CompletableFuture
的优点:
- 异步任务结束时,会自动回调某个对象的方法
- 异步任务出错时,会自动回调某个对象的方法
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
4.4 案例精讲-从电商网站的比价需求
4.4.1 回顾函数式编程
Lambda +Stream+链式调用+Java8函数式编程带走
说下 join
和 get
的区别?
join
和 get
没有区别,唯一不同点就是,join
方法无需抛出异常,get
方法需要手动抛出异常;而且二者都会阻塞。
4.4.2 业务需求
案例说明:电商比价需求
- 同一款产品,同时搜索出同款产品在各大电商的售价;
- 同一款产品,同时搜索出本产品在某一电商平台下,各个入驻门店的售价是多少
出来结果希望是同款产品在不同价格清单列表,返回一个List
《MySQL》 in jd price is 88.05
《MySQL》 in pdd price is 86.01
《MySQL》 in tabobao price is 90.43
要求深刻理解:
- 函数式编程
- 链式编程
- Stream 流式计算
方案:
经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。
我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,对于分布式微服务的调用,按照实际业务,如果是无关联 step by step 的业务,可以尝试是否可以多箭齐发,同时调用。
- step by step,查完京东查淘宝,查完淘宝查天猫…
- all 一口气同时查询。。。。。
切记,功能→性能
代码如下:
public class CompletableFutureNetMallDemo {
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("tmall"),
new NetMall("pdd"),
new NetMall("mi")
);
public static List<String> findPriceSync(List<NetMall> list, String productName) {
return list.stream()
.map(netMall -> String.format(productName + " %s price is %.2f", netMall.getNetMallName(), netMall.getPriceByName(productName)))
.collect(Collectors.toList());
}
public static List<String> findPriceASync(List<NetMall> list, String productName) {
return list.stream()
.map(netMall -> CompletableFuture.supplyAsync(() -> String.format(productName + " %s price is %.2f", netMall.getNetMallName(), netMall.getPriceByName(productName))))
.toList()
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<String> list1 = findPriceSync(list, "thinking in java");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime) + " 毫秒");
long startTime2 = System.currentTimeMillis();
List<String> list2 = findPriceASync(list, "thinking in java");
for (String element : list2) {
System.out.println(element);
}
long endTime2 = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime2 - startTime2) + " 毫秒");
}
}
class NetMall {
@Getter
private String netMallName;
public NetMall(String netMallName) {
this.netMallName = netMallName;
}
public double getPriceByName(String productName) {
return calcPrice(productName);
}
private double calcPrice(String productName) {
// 检索 消耗 1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble() + productName.charAt(0);
}
}
// 运行结果:
thinking in java jd price is 116.93
thinking in java tmall price is 116.13
thinking in java pdd price is 116.03
thinking in java mi price is 116.69
----costTime: 4053 毫秒
thinking in java jd price is 116.39
thinking in java tmall price is 116.40
thinking in java pdd price is 116.63
thinking in java mi price is 116.33
----costTime: 1008 毫秒
// 并且当我们增加查询的商铺的数量时,方案1的耗时会继续增加,但是方案2仍然大约为1秒
4.5 CompletableFuture常用方法
4.5.1 获得结果和触发计算
获取结果
public T get()
不见不散public T get(long timeout, TimeUnit unit)
过时不候public T getNow(T valueIfAbsent)
没有计算完成的情况下,给我一个替代结果,立即获取结果,不阻塞;计算完,返回计算完成后的结果,没算完,返回设定的 valueIfAbsent 值
public static void main(String[] args) {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 533;
});
// 去掉注释上面计算没有完成,返回444
// 开启注释上满计算完成,返回计算结果
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(completableFuture.getNow(444));
}
public T join()
主动触发计算
public boolean complete(T value)
是否打断 get 方法立即返回括号值
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 533;
});
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 当调用 CompletableFuture.get() 被阻塞的时候, complete 方法就是结束阻塞并 get() 获取设置的 complete 里面的值.
System.out.println(completableFuture.complete(444) + "\t" + completableFuture.get());
}
// 结果
true 444
4.5.2 对计算结果进行处理
thenApply
计算结果存在依赖关系,这两个线程串行化。
由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
// 当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化
CompletableFuture.supplyAsync(() -> {
SmallTool.sleepMillis(1000);
SmallTool.printTimeAndThread("111");
return 1;
}, executor).thenApply(f -> {
SmallTool.printTimeAndThread("222");
// int i = 10 / 0; // 异常情况:那步出错就停在那步。
return f + 2;
}).thenApply(f -> {
SmallTool.printTimeAndThread("333");
return f + 3;
}).thenApply(f -> {
SmallTool.printTimeAndThread("444");
return f + 4;
}).whenComplete((r, e) -> {
if (e == null) {
SmallTool.printTimeAndThread("result:" + r);
}
}).exceptionally(e -> {
SmallTool.printTimeAndThread(e.getMessage());
return null;
});
SmallTool.printTimeAndThread("-----主线程结束,END");
executor.shutdown();
}
// 结果
1672472051258 | 1 | main | -----主线程结束,END
1672472052262 | 24 | pool-1-thread-1 | 111
1672472052263 | 24 | pool-1-thread-1 | 222
1672472052263 | 24 | pool-1-thread-1 | java.lang.ArithmeticException: / by zero
handle
有异常也可以往下一步走,根据带的异常参数可以进一步处理
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CompletableFuture.supplyAsync(() -> {
SmallTool.sleepMillis(1000);
SmallTool.printTimeAndThread("111");
return 1;
}, executor).handle((f, e) -> {
SmallTool.printTimeAndThread("222");
int i = 10 / 0;
return f + 2;
}).handle((f, e) -> {
SmallTool.printTimeAndThread("333");
return f + 3;
}).handle((f, e) -> {
SmallTool.printTimeAndThread("444");
return f + 4;
}).whenComplete((r, e) -> {
if (e == null) {
SmallTool.printTimeAndThread("result:" + r);
}
}).exceptionally(e -> {
SmallTool.printTimeAndThread(e.getMessage());
return null;
});
SmallTool.printTimeAndThread("-----主线程结束,END");
executor.shutdown();
}
// 结果
1672472183766 | 1 | main | -----主线程结束,END
1672472184777 | 24 | pool-1-thread-1 | 111
1672472184777 | 24 | pool-1-thread-1 | 222
1672472184777 | 24 | pool-1-thread-1 | 333
1672472184782 | 24 | pool-1-thread-1 | 444
1672472184782 | 24 | pool-1-thread-1 | java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "f" is null
总结:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
// 当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化
CompletableFuture.supplyAsync(() -> {
SmallTool.sleepMillis(1000);
SmallTool.printTimeAndThread("111");
return 1;
}, executor).thenApply(f -> {
SmallTool.printTimeAndThread("222");
// int i = 10 / 0; // 异常情况:那步出错就停在那步。
return f + 2;
}).thenApply(f -> {
SmallTool.printTimeAndThread("333");
return f + 3;
}).thenApply(f -> {
SmallTool.printTimeAndThread("444");
return f + 4;
}).whenCompleteAsync((r, e) -> { // 使用 whenCompleteAsync 使用默认线程池
if (e == null) {
SmallTool.printTimeAndThread("result:" + r);
}
}).exceptionally(e -> {
SmallTool.printTimeAndThread(e.getMessage());
return null;
});
SmallTool.printTimeAndThread("-----主线程结束,END");
SmallTool.sleepMillis(2000);
executor.shutdown();
}
// 结果
1672472472269 | 1 | main | -----主线程结束,END
1672472473269 | 24 | pool-1-thread-1 | 111
1672472473270 | 24 | pool-1-thread-1 | 222
1672472473270 | 24 | pool-1-thread-1 | 333
1672472473270 | 24 | pool-1-thread-1 | 444
1672472473272 | 25 | ForkJoinPool.commonPool-worker-1 | result:10
4.5.3 对计算结果进行消费
接收任务的处理结果,并消费处理,无返回结果
thenAccept
CompletableFuture.supplyAsync(() -> {
int result = 0;
for (int i = 1; i <= 10; i++) {
result += i;
}
return result;
}).thenApply(number -> {
for (int i = 11; i <= 20; i++) {
number += i;
}
return number;
}).thenApply(number -> {
for (int i = 21; i <= 30; i++) {
number += i;
}
return number;
}).thenAccept(result -> SmallTool.printTimeAndThread("result:" + result));
// 结果
1672474582425 | 1 | main | result:465
任务之间的顺序执行
-
thenRun(Runnable runnable)
:任务 A 执行完执行 B,并且 B 不需要 A 的结果 -
thenAccept(Consumer action)
:任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值 -
thenApply(Function fn)
:任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值
4.5.4 对计算速度进行选用
谁快用谁
applyToEither
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("1号车在来的路上");
SmallTool.sleepMillis(1000);
return "1号车";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("2号车在来的路上");
SmallTool.sleepMillis(2000);
return "2号车";
}), r -> {
SmallTool.printTimeAndThread(r + "先来了");
return r;
}).join();
}
// 结果
1672474977959 | 25 | ForkJoinPool.commonPool-worker-2 | 2号车在来的路上
1672474977959 | 24 | ForkJoinPool.commonPool-worker-1 | 1号车在来的路上
1672474978966 | 24 | ForkJoinPool.commonPool-worker-1 | 1号车先来了
4.5.5 对计算结果进行合并
两个 CompletionStage
任务都完成后,最终能把两个任务的结果一起交给 thenCombine
来处理
先完成的先等着,等待其它分支任务
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("---come in 1");
SmallTool.sleepMillis(1000);
return 10;
}, executor).thenCombine(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("---come in 2");
SmallTool.sleepMillis(2000);
return 20;
}), (x, y) -> {
SmallTool.printTimeAndThread("---come in 3");
return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
SmallTool.printTimeAndThread("---come in 4");
return 30;
}), (x, y) -> {
SmallTool.printTimeAndThread("---come in 5");
return x + y;
}).thenAccept(r -> SmallTool.printTimeAndThread("result:" + r));
// executor.shutdown();
// 结果
1672475423414 | 24 | pool-1-thread-1 | ---come in 1
1672475423415 | 25 | ForkJoinPool.commonPool-worker-1 | ---come in 2
1672475423416 | 26 | ForkJoinPool.commonPool-worker-2 | ---come in 4
1672475425420 | 25 | ForkJoinPool.commonPool-worker-1 | ---come in 3
1672475425420 | 25 | ForkJoinPool.commonPool-worker-1 | ---come in 5
1672475425421 | 25 | ForkJoinPool.commonPool-worker-1 | result:60
4.5.6 对计算结果进行连接
thenCompose
连接两个有依赖关系的任务 结果由第二个任务返回
public static void main(String[] args) throws ExecutionException, InterruptedException {
SmallTool.printTimeAndThread("小白进入餐厅");
SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");
// thenCompose 连接两个有依赖关系的任务 结果由第二个任务返回
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(cf1.join() + ",小白开吃");
}
// 结果
1672475923655 | 1 | main | 小白进入餐厅
1672475923655 | 1 | main | 小白点了 番茄炒蛋 + 一碗米饭
1672475923658 | 24 | ForkJoinPool.commonPool-worker-1 | 厨师炒菜
1672475923658 | 1 | main | 小白在打王者
1672475923875 | 25 | ForkJoinPool.commonPool-worker-2 | 服务员打饭
1672475923983 | 1 | main | 番茄炒蛋 米饭,小白开吃
更多文章在我的语雀平台:https://www.yuque.com/ambition-bcpii/muziteng