记录在工作中遇到 一个页面的报表统计接口,因为数据组成有很多方面,每个方面对应的是一个方法,问题点:因为我的子方法涉及的有6个,怎么来减少等待时间? 处理办法就是,利用多线程,异步处理并带返回值,接下来我们利用CompletableFuture 来完成此业务。
1. CompletableFuture介绍
- Future模式的缺点
Future虽然可以实现获取异步执行结果的需求,但是它没有提供通知的机制,我们无法得知Future什么时候完成。
要么使用阻塞,在future.get()的地方等待future返回的结果,这时又变成同步操作。要么使用isDone()轮询地判断Future是否完成,这样会耗费CPU的资源。- CompletableFuture介绍
Netty、Guava分别扩展了Java 的 Future 接口,方便异步编程。
Java 8新增的CompletableFuture类正是吸收了所有Google
Guava中ListenableFuture和SettableFuture的特征,还提供了其它强大的功能,让Java拥有了完整的非阻塞编程模型:Future、Promise
和 Callback(在Java8之前,只有无Callback 的Future)。
CompletableFuture能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中。
CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。
2. CompletableFuture使用场景
3.创建异步任务
- supplyAsync执行CompletableFuture任务,支持返回值
//使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//自定义线程,根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
- runAsync执行CompletableFuture任务,没有返回值。
//使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable)
//自定义线程,根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
4. Demo试验
package com.example.thread;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @author :Frans
* @date :Created in 2022/5/10 15:31
* @description:
* @modified By:
*/
public class ThreadTest {
public int testA() throws InterruptedException {
int a = 0;
for (int i = 0; i < 10000; i++) {
a ++;
}
//模拟主线程其它操作耗时
Thread.sleep(6000);
return a;
}
public int testB() throws InterruptedException {
int a = 0;
for (int i = 0; i < 20000; i++) {
a ++;
}
//模拟主线程其它操作耗时
Thread.sleep(6000);
return a;
}
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
ThreadTest test = new ThreadTest();
long startTime = System.currentTimeMillis();
CompletableFuture<Integer> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> {
try {
return test.testA();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
});
CompletableFuture<Integer> completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> {
try {
return test.testB();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
});
Integer testa = completableUserInfoFuture.get();//获取个人信息结果
System.out.println("======testa:"+testa);
Integer testb = completableMedalInfoFuture.get();//获取勋章信息结果
System.out.println("======testb:"+testb);
System.out.println("总共用时1:" + (System.currentTimeMillis() - startTime) + "ms");
long startTime1 = System.currentTimeMillis();
Integer testa1 = test.testA();
System.out.println("======testa1:"+testa1);
Integer testa2 = test.testB();
System.out.println("======testa2:"+testa2);
System.out.println("总共用时2:" + (System.currentTimeMillis() - startTime1) + "ms");
}
}
运行结果
======testa:10000
======testb:20000
总共用时1:6069ms
======testa1:10000
======testa2:20000
总共用时2:12018ms总结
由此可见CompletableFuture 异步处理 非常好用,快速计算,让接口飞起来。
注意事项
1. Future需要获取返回值,才能获取异常信息
//如果不加 get()方法这一行,看不到异常信息 //future.get(); Future需要获取返回值,才能获取到异常信息。如果不加
get()/join()方法,看不到异常信息。小伙伴们使用的时候,注意一下哈,考虑是否加try…catch…或者使用exceptionally方法。
ExecutorService executorService = new ThreadPoolExecutor(5, 10, 5L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
int a = 0;
int b = 666;
int c = b / a;
return true;
},executorService).thenAccept(System.out::println);
- CompletableFuture的get()方法是阻塞的 CompletableFuture的get()方法是阻塞的,如果使用它来获取异步调用的返回值,需要添加超时时间~
//反例
CompletableFuture.get();
//正例
CompletableFuture.get(5, TimeUnit.SECONDS);
- 默认线程池的注意点 CompletableFuture代码中又使用了默认的线程池,处理的线程个数是电脑CPU核数-1。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。
- 自定义线程池时,注意饱和策略 CompletableFuture的get()方法是阻塞的,我们一般建议使用future.get(3, TimeUnit.SECONDS)。并且一般建议使用自定义线程池。
但是如果线程池拒绝策略是DiscardPolicy或者DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛弃异常。因此建议,CompletableFuture线程池策略最好使用AbortPolicy,然后耗时的异步线程,做好线程池隔离哈。