CompletableFuture-简单使用
-
前言
开发中遇到一个简单问题,一个接口对第三方api有依赖,而且是依赖三个,导致这个接口的响应速度完全依赖于第三方接口的响应速度,前期开发并未考虑性能问题,将三个依赖的API请求做成串行,导致的结果是接口总耗时是三个api接口耗时的总和,接口响应变得巨慢无比,前端经常说接口动不动就超时,通过性能分析所有的耗时基本都是第三方响应过慢。
第三方接口的响应时间我们无法处理,但是我们可以改变我们的调用模式使接口总耗时变短,之前使用串行调用,接口的耗时是三个API总耗时,我们将调用方式变为并行,然后等三个API都有返回时进行下一步逻辑处理,这样总耗时就变为三个API中最长耗时调用,这样接口响应将变快很多。
-
Future
怎样进行并行任务?查阅资料发现提到了Future,Future 是java1.5 出现的,算是一个古老的API,翻开源码就可以看到其介绍:
基本的意思是,能用来异步计算,可以拿到结果,可以取消任务。基本符合我们的使用场景。使用方式:
public void testFuture() { //1.建立执行器(需要根据实际业务场景创建) ExecutorService executor = Executors.newSingleThreadExecutor(); //2.提交需要执行的任务 Future<String> submitFirst = executor.submit(this::taskFirst); Future<List<String>> submitSecond = executor.submit(this::taskSecond); Future<Map<String, String>> submitThird = executor.submit(this::taskThird); //3.轮询获取执行任务完成状态 while (!submitFirst.isDone() || !submitSecond.isDone() || !submitThird.isDone()) { //todo 超时强行取消任务 } //4. 拿到结果进行后处理 try { String resultFirst = submitFirst.get(); List<String> resultSecond = submitSecond.get(); Map<String, String> resultThird = submitThird.get(); // TODO: 处理结果 } catch (Exception e) { e.printStackTrace(); } }
注意到我们拿到执行结果的方式,轮询!!轮询的方式耗费无谓的CPU资源,而且也不能及时地得到计算结果,是否能通过
别的方式获取到结果?还可以通过阻塞的方式获取到结果,我们既然使用异步处理任务又需要阻塞主线程获取结果,与初衷
完全背道而驰了。如果我们的子任务出现异常,在Future中没有提供异常处理的API的,处理异常也是个很麻烦的问题。
所以说Future是有缺陷的。
-
CompletableFuture
怎样弥补Future缺陷?Java8给出了答案,使用CompletableFuture。
简单查阅下源码说明:
我们注意到有提到 CompletionStage,简单看下介绍:
大致意思是:定义了一组接口用于在一个阶段执行结束之后,要么继续执行下一个阶段,要么对结果进行转换产生新的结果。个人
理解有点像Java8的Stream。CompletableFuture 实现了Future 与 CompletionStage 为了就是弥补 Future的各种不足。
使用方式:
public void testCompletableFuture() { //1.建立执行器(需要根据实际业务场景创建) ExecutorService executor = Executors.newSingleThreadExecutor(); //2.添加任务 CompletableFuture<String> firstCompletableFuture = CompletableFuture .supplyAsync(this::taskFirst, executor) .exceptionally(throwable -> null); CompletableFuture<List<String>> secondCompletableFuture = CompletableFuture .supplyAsync(this::taskSecond, executor); CompletableFuture<Map<String, String>> thirdCompletableFuture = CompletableFuture. supplyAsync(this::taskThird, executor); //依赖与任务一的完成 CompletableFuture<String> lastCompletableFuture = firstCompletableFuture .whenCompleteAsync((s, throwable) -> taskThird()) .exceptionally(throwable -> null); //3. 合并所有任务 CompletableFuture<Void> totalFuture = CompletableFuture.allOf(firstCompletableFuture, secondCompletableFuture, thirdCompletableFuture, lastCompletableFuture); try { //4.所有任务完成,可以选择指定总超时时间 totalFuture.get(4, TimeUnit.SECONDS); //5.获取每一个任务的结果 String firstResult = firstCompletableFuture.get(1, TimeUnit.SECONDS); List<String> secondResult = secondCompletableFuture.get(1, TimeUnit.SECONDS); Map<String, String> thirdResult = thirdCompletableFuture.get(1, TimeUnit.SECONDS); String lastResult = lastCompletableFuture.get(1, TimeUnit.SECONDS); //todo 处理结果 } catch (Exception e) { e.printStackTrace(); } }
用起来比Future更加简单,获取结果的方式也更加又优雅,并且支持异常的处理。查看CompletableFuture的使用教程发现
CompletableFuture支持各种任务组合使用,提供各种强大的API,使用起来更加灵活。 -
想法
抛开CompletableFuture使用,回到文章开始的场景,其实可以理解为三个任务并行,它们之间没有多大关系,但是它们的结果又决定最终的大结果,换个角度理解为:我们把一个大的任务分成了三个小任务,三个小任务各自负责自己的模块,各个任务的结果合并成为大任务的结果,这种场景可以认为是分治思想的使用,其实细想,代码中很多地方都可以认为用到分治的思想,我们总是把很多方法的结果合并起来达到最终结果,这样也是分治。工作中生活中我们也会把一些大的任务拆分成很多细小的任务,单个执行,最终合并结果,这样任务会有条不紊的完成而不会变得错综复杂。
-
参考文档