CompletableFuture异步编程分析(美团文章CompletableFuture分析)

I/O密集型服务显然需要并行从下游获取

今天写了个接口,需要调用多个接口获取结果的,于是想到了completableFuture来并行调用多个接口,使用的过程中产生了一些疑惑研究了一下:

  1. 是否需要传入线程池参数
  2. 使用completableFuture和直接使用线程池有什么区别
  3. 如果调用completableFuture出现了线程异常怎么办

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方

CompletableFuture使用

可组合:可以将多个依赖操作通过不同的方式进行编排,例如CompletableFuture提供thenCompose、thenCombine等各种then开头的方法,这些方法就是对“可组合”特性的支持

在这里插入图片描述

ExecutorService executor = Executors.newFixedThreadPool(5);

// 1、使用supplyAsync发起**异步调用**
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "result1", executor);(异步调用创建使用)
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "result2", executor)

/*
supplyAsync执行CompletableFuture任务,支持返回值
runAsync执行CompletableFuture任务,没有返回值
*/

//thenApplyAsync:会在前一个CompletableFuture完成后异步应用这个函数
CompletableFuture<String> cf3 = cf1.thenApplyAsync(result1 -> "result3_" + result1, executor);
CompletableFuture<String> cf5 = cf2.thenApplyAsync(result2 -> "result5_" + result2, executor);

CompletableFuture<String> cf4 = cf1.thenCombineAsync(cf2, (result1, result2) -> "result4_" + result1 + "_" + result2, executor);

//CompletableFuture.allOf方法接受多个CompletableFuture对象,并返回一个新的CompletableFuture<Void>对象,该对象在所有给定的CompletableFuture对象完成后完成
CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);


CompletableFuture<String> result = cf6.thenApply(v -> {
  // 这里的thenApply会在CF3、CF4、CF5全部完成时执行
  String result3 = cf3.join(); // 这里使用join是安全的,因为allOf已经确保了cf3完成
  String result4 = cf4.join(); 
  String result5 = cf5.join(); 
  // 根据result3、result4、result5组装最终result
  return "finalResult_" + result3 + "_" + result4 + "_" + result5;
})

异步编排假如有一个任务出现异常怎么办
在这里插入图片描述

  1. 自定义线程池时,注意饱和策略
    CompletableFuture的get()方法是阻塞的,我们一般建议使用future.get(3, TimeUnit.SECONDS)。并且一般建议使用自定义线程池。
    但是如果线程池拒绝策略是DiscardPolicy或者DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛弃异常。因此建议,CompletableFuture线程池策略最好使用AbortPolicy,然后耗时的异步线程,做好线程池隔离哈

CompletableFuture的设计思想

原理分析从“观察者”和“被观察者”两个方面着手
在这里插入图片描述

被观察者

每个CompletableFuture都可以被看作一个被观察者
变量stack:一个Completion类型的链表成员,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者。上面例子中步骤fn2就是作为观察者被封装在UniApply中。
result:用来存储返回结果数据。这里可能是一次RPC调用的返回值,也可能是任意对象,在上面的例子中对应步骤fn1的执行结果。

观察者

CompletableFuture支持很多回调方法,例如thenAccept、thenApply、exceptionally等,这些方法接收一个函数类型的参数,生成一个Completion类型的对象(即观察者),并将入参函数赋值给Completion的成员变量fn,然后检查当前CF是否已处于完成状态(即result != null),如果已完成直接触发fn,否则将观察者Completion加入到CF的观察者链stack中,再次尝试触发,如果被观察者未执行完则其执行完毕之后通知触发。

观察者中的dep属性:指向其对应的CompletableFuture,在上面的例子中dep指向CF2。
观察者中的src属性:指向其依赖的CompletableFuture,在上面的例子中src指向CF1。
观察者Completion中的fn属性:用来存储具体的等待被回调的函数。这里需要注意的是不同的回调方法(thenAccept、thenApply、exceptionally等)接收的函数类型也不同

CompletableFuture 注意事项

1. CompletableFuture支持同步和异步

CompletableFuture实现了CompletionStage接口,通过丰富的回调方法,支持各种组合操作,每种组合场景都有同步和异步两种方法。

同步方法(无Async后缀的方法):

  • 如果注册时被依赖的操作已经执行完成,则直接由当前线程执行
  • 如果注册时被依赖的操作还未执行完,则由回调线程执行

异步方法(Async后缀的方法):

  1. 可以选择是否传递线程池参数Executor运行在指定线程池中;
  2. 当不传递Executor时,会使用ForkJoinPool中的共用线程池CommonPool(CommonPool的大小是CPU核数-1,如果是IO密集的应用,线程数可能成为瓶颈)。
ForkJoinPool使用原理

1.ForkJoinPool概念
ForkJoinPool是jvm提供的一个用于并行执行的任务框架。其主旨是将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果。得到最终的结果。java8的stream中使用比较多,ForkJoin机制只能在单个jvm上运行

ForkJoinPool采用了分治法和工作窃取算法,避免工作线程由于拆分了任务之后的join等待过程。这样处于空闲的工作线程将从其他工作线程的队列中主动去窃取任务来执行

1.1 分治法(空间换时间)
分治法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题的相互独立且与原问题的性质相同,求出子问题的解之后,将这些解合并,就可以得到原有问题的解。时间复杂度O(log n)。那么对应到ForkJoinPool对问题的处理也如此。
分支法示例图:
在这里插入图片描述
将一个大的任务,通过fork方法不断拆解,直到能够计算为止,之后,再将这些结果用join合并。逐次递归,就得到了我们想要的结果

1.2 工作窃取(work-stealing)
工作窃取是指当某个线程的任务队列中没有可执行任务的时候,从其他线程的任务队列中窃取任务来执行,充分利用工作线程的计算能力,减少线程由于获取不到任务而造成的空闲浪费。在ForkJoinpool中,工作任务的队列都采用双端队列Deque容器。我们知道,在通常使用队列的过程中,我们都在队尾插入,而在队头消费以实现FIFO。而为了实现工作窃取。一般我们会改成工作线程在工作队列上LIFO,而窃取其他线程的任务的时候,从队列头部取获取。示意图如下:

在这里插入图片描述

工作线程worker1、worker2以及worker3都从taskQueue的尾部popping获取task,而任务也从尾部Pushing,当worker3队列中没有任务的时候,就会从其他线程的队列中取stealing,这样就使得worker3不会由于没有任务而空闲。这就是工作窃取算法的基本原理

2. 构造completable需要传入线程池参数

当不传递线程池时,会使用ForkJoinPool中的公共线程池CommonPool,所有调用将共用该线程池,核心线程数=处理器数量-1(单核核心线程数为1),所有异步回调都会共用该CommonPool
核心与非核心业务都竞争同一个池中的线程,很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,做线程池资源隔离,减少不同业务之间的相互干扰

3. 异常处理

由于异步执行的任务在其他线程上执行,而异常信息存储在线程栈中,因此当前线程除非阻塞等待返回结果,否则无法通过try\catch捕获异常。CompletableFuture提供了异常捕获回调exceptionally,相当于同步调用中的try\catch。使用方法如下所示:

@Autowired
private WmOrderAdditionInfoThriftService wmOrderAdditionInfoThriftService;//内部接口
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
    CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
    return remarkResultFuture
          .thenApply(result -> {//这里增加了一个回调方法thenApply,如果发生异常thenApply内部会通过new CompletionException(throwable) 对异常进行包装
      //这里是一些业务操作
        })
      .exceptionally(err -> {//通过exceptionally 捕获异常,这里的err已经被thenApply包装过,因此需要通过Throwable.getCause()提取异常
         log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, ExceptionUtils.extractRealException(err));
         return 0;
      });
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值