前言
最近遇到了一个业务场景,涉及到多数据源之间的请求的流程编排,正好看到了一篇某团介绍CompletableFuture原理和使用的技术文章,主要还是涉及使用层面。网上很多文章涉及原理的部分讲的不是特别详细且比较抽象。因为涉及到多线程的工具必须要理解原理,不然一旦遇到问题排查起来就只能凭玄学,正好借此梳理一下CompletableFuture的工作原理
背景
我们把Runnable理解为最基本的线程任务,只具备在线程下执行一段逻辑的能力。为了获取执行的返回值,创造了Callable和与其配合使用的Future。为了将任务之间进行逻辑编排,就诞生了CompletableFuture。关于如何理解任务的逻辑编排,举一个简单的例子:
打开电脑-更新系统这两个操作是有先后顺序的,但是泡茶和这两个操作没有先后顺序,是可以并行的,而开始办公必须要等待其他操作结束之后才能进行,这就形成了任务编排的执行链。
在IO密集型系统中,类似的场景有很多。因为不同数据集的查询依赖主键不同,A数据集的查询主键是B数据集的一个字段这种情况很常见,通常还需要并发查询多个数据集的数据,所以对于多线程的执行编排是有需求的。
一种解决办法是CountDownLatch,让线程执行到某个地方后进行等待,直到依赖的任务执行结束。对于一些简单的执行链是可以满足的,但是当编排逻辑复杂起来,CountDownLatch会导致代码难以维护和调试。所以诞生了CompletableFuture用来描述和维护任务之间的依赖关系以进行任务编排。在实际应用中,有以下两类场景是适合使用任务编排的:
-
多数据源请求的流程编排
-
非阻塞化网关等NIO场景
使用方式
创建与执行
同步方法
和FutureTask类似,CompletableFuture也通过get()方法获取执行结果。但是不同的是,CompletableFuture本身可以不承载可执行的任务(相比FutureTask则必须承载一个可执行的任务Callable),通过一个用于标记执行成功并设置返回值的函数,在使用上也更为灵活,如下:
CompletableFuture<String> demo = new CompletableFuture<>();
demo.complete("success");
System.out.println(demo.get());
复制代码
执行结果:success
和Future类似,get()函数也是同步阻塞的,调用get函数后线程会阻塞直到调用complete方法标记任务已经执行成功。
除了手动触发任务的完成,也可以让创建对象的同时就标记任务完成:
CompletableFuture<String> demo = CompletableFuture.completedFuture("success");
System.out.println(demo.get());
复制代码
执行结果:success
异步方法
相比于同步方法,异步执行更为常见。比如下面这个例子:
CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
return "success";
});
System.out.println(demo.get());
复制代码
执行结果:
do something by threadForkJoinPool.commonPool-worker-9
success
supplyAsync方法接收一个Supplier对象,逻辑函数交给线程池中的线程异步执行
默认会使用ForkJoinPool的公共线程池来执行代码(不推荐),当然也可以指定线程池,如下:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
return "success";
}, executor);
System.out.println(demo.get());
复制代码
执行结果:
do something by threadpool-1-thread-1
success
如果不需要执行结果,也可以用runAsync方法:
CompletableFuture.runAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
});
复制代码
执行结果:
do something by threadForkJoinPool.commonPool-worker-9
多任务编排
多任务编排是CompletableFuture的核心,这里列举不同的场景来进行说明
一元依赖
步骤2需要依赖步骤1执行完毕才能执行,类似主线程的顺序执行,可以通过以下方式实现:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤1】");
return "【步骤1的执行结果】";
}, executor);
CompletableFuture<String> step2 = step1.thenApply