在日常需求的研发过程中都遇到过需要调用多个服务并将他们的结果进行合并的需求。这种情形又分为多个结果相互不依赖的情况,以及多个调用的结果相互依赖。比如一个首页,需要获取用户信息、首页banner信息、推荐信息、用户资产信息等。其中获取用户信息和获取banner信息、推荐信息是相互独立的没有依赖关系,而获取用户信息和用户资产以及资产详情信息是相互依赖的,需要先获取到用户的信息,然后获取用户资产依赖于用户信息,获取资产详情又依赖于用户资产信息。
对于上面的问题,最简单普遍的处理方案为:获取用户信息-->获取用户资产-->获取资产详情-->获取首页推荐-->获取首页banner。将这些过程进行顺序调用获取到结果之后返回。但是获取用户信息和获取首页其它信息直接并没有关联关系,顺序调用的话线程一个一个顺序执行,虽然不相关但是却必须等到前面的方法执行完毕之后才能执行后面的方法。这种方式导致调用耗时就是所有调用的总和。还有一种方式就是不想关的任务并发执行,这样A线程在查询用户信息的时候B线程也不闲着去查询首页banner信息,C线程去查询推荐信息。这样就相当于多人工作,耗时也就少了,尤其对于首页这种对于用户体验影响特别大的耗时尤为重要。
可以利用异步编程工具CompletableFuture、CompleteService实现需求,利用现有的工具可以简单的就实现功能。CompletableFuture、CompleteService都是异步编程工具。但是他们的使用场景有所不同。CompletableFuture适用于有相互依赖关系的多个任务之间的编排,而CompleteService适用于多个任务之间没有相互依赖关系的情景。接下来就看一下如何适用这两个异步编程工具实现需求。
1、CompletableFuture
CompletableFuture实现Future和CompletionStage接口。 Future接口比较熟悉了,有返回值的线程实现方式种Callalbe接口会返回一个Future对象,调用Future对象的get方法就可以获取到线程的返回值。但是get方法是阻塞的,需要等到线程执行完毕之后才能够等到结果并返回,不然就会一直阻塞。由此可见CompletableFuture也有阻塞的get方法来获取线程的结果。CompletionStage接口是CompletableFuture实现任务编排和链式调用的关键。
CompletionStage接口种定义了许多链式调用的方法,这些方法返回的都是CompletionStage类型的对象,因为返回的都是CompletionStage类型的对象所以可以继续调用CompletionStage中的方法,这也是链式调用的关键。(这里对我们编码也有一些启示,我们在编写的代码的时候,如果也想设计成这种链式编程的代码风格,在封装方法的时候就可以参考这种代码格式,比如建造者模式就是每次调用都会返回Builder对象,然后继续调用其它的构造流程的方法。)
了解了CompletableFuture的基本组成以及方法,接下来就来进行链式编程的代码编写。
CompletableFuture voidCompletableFuture =
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第一个任务执行结束");
}).thenRun(() -> {
System.out.println("第二个任务开始执行");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 阻塞等待任务执行完成
Object o = voidCompletableFuture.get();
System.out.println("任务执行结束"+o);
任务的执行结果为:
第一个任务执行结束
第二个任务开始执行
任务执行结束null
通过执行结果看出来,第一个任务和第二个任务之间是链式执行的,必须第一个任务执行完成之后才能执行第二个任务。并且最后任务是没有返回值的。进入方法的源码
发现这个方法传入的是一个Runnable,所以任务是没有返回值,调用CompletableFuture的get方法返回的也是空。如果需要获取任务的返回值,可以调用其它的方法。
supplyAsync方法接收一个有返回值的Supplier,返回一个CompletableFuture,这个Future中即是线程执行的结果。
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第一个任务执行结束");
return "这是第一个任务的结果";
}).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
System.out.println("第二个任务开始执行");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("获取到第一个任务得结果:"+s);
return "这是第二个任务的结果";
}
});
// 阻塞等待任务执行完成
Object o = voidCompletableFuture.get();
System.out.println("任务执行结束"+o);
执行结果:
第一个任务执行结束
第二个任务开始执行
获取到第一个任务得结果:这是第一个任务的结果
任务执行结束这是第二个任务的结果
supplyAsync方法是有返回值的,从运行的结果看出来,两个任务之间也是链式调用的,只有第一个任务执行完成之后才会执行第二个任务,第二个任务中传入第一个任务的结果,最后返回的Future为第二个任务的结果。为了更好的理解CompletableFuture是如何进行链式调用的,进入方法进行分析
1.1、CompletableFuture的原理
supplyAsync方法需要传入一个Supplier<U>函数式接口,也就是带返回的方法(Callable正是这种格式)内部调用asyncSupplyStage方法,该方法需要一个asyncPoll,看一下这个asyncPoll是什么。
ayncPool是一个ForkJoinPool。
将任务包装成AsyncSupply,然后放入ayncPool中执行。
AsyncSupply是一个静态内部类并且继承ForkJoinTask。回到上面的方法,线程池ayncPool调用execute方法执行任务。并将结果放入到result变量中。AsyncSupply调用完成之后再调用thenApply执行任务二。
thenRun和thenApply方法的区别是thenRun参数为一个没有入参没有返回值的接口函数,二thenApply需要接收一个参数并且返回一个参数,看一下具体的执行流程。调用uniApplyStage 方法,看一下具体的方法。
关键在于调用的uniApply方法,这个方法传入上一个任务返回的ComplteableFuture对象
方法中获取上一个任务的 ComplteableFuture中的结果,如果结果不为空则说明上一个任务已经执行完毕,否则表示上一个任务未执行完成返回false。重新看上一张代码片段,当返回false也就是上一个任务没有执行完成的情况。调用了push方法将任务放入了任务队列中,当任务执行完成之后在从任务队列中获取任务执行。这就是链式执行的原理。
ComplteableFuture不仅提供了如applyAysnc、thenApply、thenApplyAsync(于thenApply的区别在于第二个任务当前方法会适用另外的线程,thenApply会使用同一个线程),而且提供了exceptionally、handle等异常处理方法可以获取线程执行的异常并进行处理。ComplteableFuture还有网状执行的方法如:allof-所有任务执行完成之后在执行后面的任务、anyof任意一个任务执行完成之后则执行后面的任务等方法。
2、CompleteService
上面介绍了ComplteableFuture支持链式任务执行,任务之间有依赖关系并且有执行的先后顺序,而CompleteService则是支持并发执行任务,任务之间没有依赖关系,并且没有先后顺序的要求,对于结果的处理也是先执行完成的任务先处理结果。(今天下班后续补充)