当前位置: 首页 > 科技观察

CompletableFuture真香,可以替代CountDownLatch!

时间:2023-03-12 19:27:46 科技观察

在类命名的长篇文章中,我们提到了Future和Promise。Future相当于一个占位符,代表一个操作的未来结果。一般get可以直接阻塞获取结果,或者让它异步执行然后通过callback回调结果。但是如果回调嵌入在回调中呢?如果层次很深,就是回调地狱。Java中的CompletableFuture其实就是Promise,用来解决回调地狱问题。存在使代码美观的承诺。多么美丽?这么说吧,一旦用上了CompletableFuture,就爱不释手,就像初恋女友一样,天天想着她。一系列的静态方法从它的源码我们可以看出CompletableFuture直接提供了几个方便的静态方法入口。有运行和供应两组。run的参数是Runnable,supply的参数是Supplier。前者没有返回值,后者有,否则没有区别。这两组静态函数都提供了传入自定义线程池的功能。如果你没有使用外部线程池,那么它将使用默认的ForkJoin线程池。你无法控制默认的线程池,它的大小和使用,所以建议你自己传一个。典型的代码,这样写的。CompletableFuturefuture=CompletableFuture.supplyAsync(()->{return"test";});Stringresult=future.join();得到CompletableFuture之后,就可以做更多的花样了。这些技巧有很多。我们已经面对面地谈过了。CompletableFuture的主要作用是让代码好看。有了Java8之后的流,整个计算过程就可以抽象成一个流。前面任务的计算结果可以直接作为后面任务的输入,就像管道一样。thenApplythenApplyAsyncthenAcceptthenAcceptAsyncthenRunthenRunAsyncthenCombinethenCombineAsyncthenComposethenComposeAsync比如下面这段代码的执行结果是99,代码执行的顺序没有被打乱,因为是异步的。CompletableFuturecf=CompletableFuture.supplyAsync(()->10).thenApplyAsync((e)->{try{Thread.sleep(10000);}catch(InterruptedExceptionex){ex.printStackTrace();}返回*10;}).thenApplyAsync(e->e-1);cf.join();System.out.println(cf.get());同样,function的作用取决于then后面的动词。apply有输入参数和返回值,输入参数是前置任务的输出。accept有入参无返回值,会返回CompletableFuture运行。在没有输入参数和返回值的情况下,它也会返回CompletableFuturecombine组成一个复合结构,连接两个CompletableFuture,将它们的两个输出结果作为combine的输入compose,将嵌套的CompletableFuture展平,拼接两个CompletableFuturewhen和上面的函数列表手柄。事实上,还有更多。例如:whenCompletewhen表示任务完成时的回调。例如,在我们上面的示例中,我们打算在完成任务后输出一个done。也属于只有输入参数没有输出参数的范畴,适合最后一步观察。CompletableFuturecf=CompletableFuture.supplyAsync(()->10).thenApplyAsync((e)->{try{Thread.sleep(1000);}catch(InterruptedExceptionex){ex.printStackTrace();}返回*10;}).thenApplyAsync(e->e-1).whenComplete((r,e)->{System.out.println("done");});cf.join();System.out.println(cf.get());handle和exceptionally的功能和whenComplete非常相似。publicCompletableFutureexceptionally(Functionfn);publicCompletionStagehandle(BiFunctionfn);CompletableFuture的tasks被concaten起来,如果其中一个步骤在某个step发生异常,会影响后续代码的运行。从名字就可以看出,exceptionally就是专门为应对这种情况而设计的。比如我们强行将某个step除0,就会出现异常,捕获后返回-1,继续运行。CompletableFuturecf=CompletableFuture.supplyAsync(()->10).thenApplyAsync(e->e/0).thenApplyAsync(e->e-1).exceptionally(ex->{System.out.println(ex);return-1;});cf.join();System.out.println(cf.get());handle更高级,因为它除了异常参数外,还有一个正常的输入参数。处理方法也类似,这里不再赘述。当然,CompletableFuture的功能不止这些,还有更多,根据函数名就很容易理解它的功能。它还可以替代复杂的CountDownLatch,后者涉及多个棘手的函数。请考虑以下场景,而不是CountDownLatch。某个业务接口需要处理数百个请求,然后在请求之后汇总结果。如果按顺序执行,假设每个接口耗时100ms,那么100个接口耗时10秒。如果我们并行获取,那么效率会提高。使用CountDownLatch可以解决。ExecutorServiceexecutor=Executors.newFixedThreadPool(5);CountDownLatchcountDown=newCountDownLatch(requests.size());for(Requestrequest:requests){executor.execute(()->{try{//someopts}finally{countDown.countDown();}});}countDown.await(200,TimeUnit.MILLISECONDS);我们使用CompletableFuture来替换它。ExecutorServiceexecutor=Executors.newFixedThreadPool(5);List>futureList=requests.stream().map(request->CompletableFuture.supplyAsync(e->{//someopts},executor)).collect(Collectors.toList());CompletableFutureallCF=CompletableFuture.allOf(futureList.toArray(newCompletableFuture[0]));allCF.join();我们这里使用了一个main函数,也就是allOf,用来合并所有的CompletableFuture被合并;同样,还有anyOf,表示只会运行其中一个。常用的,还有另外三个函数:thenAcceptBoth处理两个任务,有两个任务结果为参数,无返回值,没有返回值End自从接触了CompletableFuture之后,我就很少硬编码Future了。相较于各种回调的嵌套,CompletableFuture为我们提供了更加直观和美观的API。在“多任务等待完成”的应用场景中,CompletableFuture成为了我的首选。唯一的问题是它的功能有点丰富,你需要适应一段时间。此外,还有一个小问题。我个人觉得这个类如果叫Promise的话,可以和JS统一,是锦上添花。作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。