文章目录
1.先来聊聊为什么要使用CompletableFuture?
那就先聊聊Future的局限性:
Future表示一个异步计算的结果。提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。但是从框架本身会存在下列问题:
- 多个任务进行链式调用无法支持:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
- 组合任务编排能力无法实现:如果你运行了100个任务,并且按照一定的顺序编排起来,future没有办法去实现。
- 没有异常处理的钩子:Future接口中没有关于异常处理的方法;
2. 如何高效利用CompletableFuture实现任务编排?
先整两个方法:打印时间方法和睡眠方法,方便后面直接调用
public static ExecutorService executor = Executors.newCachedThreadPool();
private static void sleep(long sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
};
}
private static void currentDate(String str) {
// 创建一个Date对象,它包含了当前时间
Date now = new Date();
// 创建一个SimpleDateFormat对象,用于指定输出格式
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 使用format方法将Date对象格式化为字符串
String currentTime = dateFormat.format(now);
// 打印当前时间
System.out.println(str + "是: " + currentTime);
}
- supplyAsync的使用
//get()方法抛出的是经过检查的异常,ExecutionException, InterruptedException需要用户手动处理或者捕获异常
@Test
public void test1() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "hello,word";
},executor);
String s = null;
try {
s = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(s);
}
- 正常执行和发生异常如何处理
//CompletableFuture的计算结果完成,需要进行的操作 whenComplete
//抛出异常时,需要进行的操作exceptionally
@Test
public void test2() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
sleep(1000);
int i = 1/0;
return "hello,word";
});
//正常执行完
future.whenComplete((s, throwable) -> System.out.println(s));
//发生异常
future.exceptionally(t -> {
System.out.println("执行失败:" + t.getMessage());
return "异常xxxx";
});
future.join();
}
- 两个任务组合
其实下面的代码可以执行下,会发现整体使用的时间会是执行时间较长的任务的时间
@Test
public void test3() throws ExecutionException, InterruptedException {
currentDate("执行前");
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return 1;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return 2;
});
CompletableFuture<Integer> future = future1.thenCombine(future2, Integer::sum);
Integer result = future.get();
System.out.println(result);
currentDate("执行后");
}
执行结果如下:
可以从结果看到整体执行时间就是执行任务较长的时间,并且直接结果为3
- 多任务组合
两个线程任务相比较,先获得执行结果的,就对该结果进行下一步的转化操作。
@Test
public void test5() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
int number = new Random().nextInt(10);
System.out.println("future1 start:" + number);
sleep(number);
System.out.println("future1 end:" + number);
return number;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
int number = new Random().nextInt(10);
System.out.println("future2 start:" + number);
sleep(number);
System.out.println("future2 end:" + number);
return number;
});
CompletableFuture<Integer> future = future1.applyToEither(future2, new Function<Integer, Integer>() {
@Override
public Integer apply(Integer number) {
System.out.println("最快结果:" + number);
return number;
}
});
Integer result = future.get();
System.out.println("结果值为 " + result);
}
执行结果
future1和future2的睡眠时间是一个随机数,从执行日志可以看到生成的数小,睡眠时间越少,越先被执行完,因此future拿到的是最先执行完的结果5.
- 个数不定的CompletableFuture
可以使用allOf().join等待所有异步任务的执行。
@Test
public void test6() throws ExecutionException, InterruptedException {
currentDate("执行前");
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return 1;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return 2;
});
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
sleep(3000);
return 3;
});
//阻塞等待执行
CompletableFuture.allOf(future1, future2, future3).join();
Integer min = Stream.of(future1, future2, future3).map(item -> {
try {
return item.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}).sorted().findFirst().orElseThrow(() -> new RuntimeException("没有最小值"));
System.out.println("最小值为 " + min);
currentDate("执行后");
}
执行结果
可以看出执行时间为3秒,就是执行时间最长的子线程的执行时间。
3. 那些年我双手插兜,不料踩入的坑!
1. join()和get()的异同:
join()和get()方法都是用来获取CompletableFuture异步之后的返回值
join()方法抛出的是uncheck异常(比如RuntimeException),不会强制开发者去捕获处理,会将异常包装成CompletionException异常 /CancellationException异常,但是本质原因还是代码内存在的真正的异常,
get()方法抛出的是经过检查的异常,ExecutionException, InterruptedException 需要用户手动try catch处理。
这个小细节可以省去很多显式处理异常的冗余代码,不过具体看应用场景啦!!
2. 捕获异常的坑
如果你在遇到CompletableFuture抛出异常的时候一不注意可能会try catch错异常。
话不多说,举个例子
- 先来个并发工具类
public class CompletableFutureEngine {
private final static ExecutorService executorService = Executors.newFixedThreadPool(4);
/**
* 创建并行任务并执行
*
* @param list 数据源
* @param function API调用逻辑
* @param exceptionHandle 异常处理逻辑
* @return 处理结果列表
*/
public static <S, T> List<T> parallelFutureJoin(Collection<S> list, Function<S, T> function, Consumer<Throwable> exceptionHandle) {
List<CompletableFuture<T>> completableFutures = list.stream()
.map(s -> CompletableFuture.supplyAsync(() -> function.apply(s)))
.collect(Collectors.toList());
List<T> results = new ArrayList<>();
try {
CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<T> completableFuture : completableFutures) {
results.add(completableFuture.get());
}
} catch (Exception e) {
exceptionHandle.accept(e);
}
return results;
}
}
写完之后你感觉很优雅,里面用了各种泛型,并且当程序执行报错的时候会回调exceptionHandle.accept(e),把错误传给调用测去做处理,此时你迫不及待去写个demo赶紧测试下:
public class EngineDemo {
private static void sleep(long sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void currentDate(String str) {
// 创建一个Date对象,它包含了当前时间
Date now = new Date();
// 创建一个SimpleDateFormat对象,用于指定输出格式
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 使用format方法将Date对象格式化为字符串
String currentTime = dateFormat.format(now);
// 打印当前时间
System.out.println(str + "是: " + currentTime);
}
public static void main(String[] args) {
currentDate("执行前");
List<Integer> numList = CompletableFutureEngine.parallelFutureJoin(Arrays.asList(1, 3, 5),
num -> {
sleep(num * 1000);
if (num == 1 || num == 3) {
throw new BusinessException("心别太大");
}
return num;
}
, e -> {
if (e instanceof BusinessException) {
System.out.println("BusinessException =" + e.getMessage());
} else {
System.out.println("Not BusinessException");
}
}
);
System.out.println(numList);
currentDate("执行后");
}
}
打印结果:
通过日志会发现子线程抛出异常BusinessException时,主线程捕获,但是发现捕获完之后抛给调用端处理,调用端判断是否是BusinessException,如果不是,打印日志Not BusinessException,恰好控制台Not BusinessException,说明调用端捕获的并不是BusinessException,那主线程抛出的是什么异常呢?我们可以去debug一下。
会发现捕获的异常为CompletionException,这个异常的里面有个cause属性,里面封装的就是子线程抛出的异常。我们可以再改下工具类试下!!
public class CompletableFutureEngine {
private final static ExecutorService executorService = Executors.newFixedThreadPool(4);
/**
* 创建并行任务并执行
*
* @param list 数据源
* @param function API调用逻辑
* @param exceptionHandle 异常处理逻辑
* @return 处理结果列表
*/
public static <S, T> List<T> parallelFutureJoin(Collection<S> list, Function<S, T> function, Consumer<Throwable> exceptionHandle) {
List<CompletableFuture<T>> completableFutures = list.stream()
.map(s -> CompletableFuture.supplyAsync(() -> function.apply(s)))
.collect(Collectors.toList());
List<T> results = new ArrayList<>();
try {
CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<T> completableFuture : completableFutures) {
results.add(completableFuture.get());
}
} catch (Exception e) {
exceptionHandle.accept(e.getCause());
}
return results;
}
}
在运行调用端,日志如下:
你会发现,调用端可以正常捕获异常了
从表面上看是CompletionException对BusinessException封装了一层,为了证实下,我们可以去看下源码:
当抛出异常之后这块会进行封装,你会发现这块ex就等于BusinessException,那我们接下来看看d.completeThrowable(ex)里面的执行逻辑,会发现在最后调用了CompletionException的构造方法把异常封装起来向外抛出。
大家感觉恍然大悟了没?是否感觉之前在用的时候没有注意这么多细节,
感觉对您有所启发的话,记得帮忙点赞,收藏加关注,并且分享给需要的小伙伴!!一起加油!!后面干货满满