CompletableFuture原理与实践(美团技术团队文章转载&部分个人理解与思考)

本篇文章来自于对美团技术团队文章的理解+二次创作

原文章:

美团技术团队原文

背景

随着订单、任务量的持续提升,各个系统服务面临的压力也是越来越大。在我们最近研究的优惠券系统当中,我们的优惠券结算功能(当前订单可用/不可用优惠券&相应的优惠金额)提供了接口,最终所有的订单都会调用这个接口。对外,我们向订单结算/购物车/商品界面提供这个功能接口,对内,我们调度其他的各个下游服务获取数据、进行聚合,属于是一个io密集型任务。这个任务调用的规模可能达到百万级别,更大的系统像美团外卖系统订单每日可以达到千万级别,这些情况下,使用同步加载方式的弊端就会逐渐显现。我们开始考虑使用并行加载替代同步加载。

为什么需要并行加载

以我们的优惠券系统的结算功能为例

redis的网络往返通常在毫秒级别,而JSON解析、反射等操作的延迟在微秒级别,整体瓶颈依旧在网络io上面,可以认为是io密集型。

具有以下鲜明特征:

  1. cpu利用率较低,等待时间较长:

    整个任务的生命周期当中,cpu真正执行计算(加减乘除、逻辑判断)的时间实际上较短,更多时间可能用在等待外部资源、处在阻塞状态下

  2. 瓶颈并不在于cpu,而是外部资源

除此之外,这个业务还有个特点:

  • 服务端需要一次性返回订单可用/不可用优惠券相关信息&每个优惠券的优惠金额(更复杂的甚至涉及多个优惠券叠加的优惠金额);对每张优惠券进行服务调用并不现实,服务端体验会很差,而且效率很低。

优惠券系统流量如此大的情况下,为了保障商家的用户体验,保障接口的高性能,并行从下游获取数据成为了必然。

并行加载方式

并行从下游获取数据,从io模型上面分为两类:同步 模型&异步模型

同步模型

从各个服务获取数据最常见的就是同步调用:

实现起来较为简单,但是接口耗时时间长、性能低,接口响应时间:T > T1+T2+T3+......+Tn

这时候为了缩短接口响应时间,我们会使用线程池(最经典的ThreadPoolExecutor等)

这种事方式由于下面两个原因会导致资源利用效率较低

  • CPU资源被大量浪费在阻塞等待 上,导致CPU资源利用率较低。在Java8之前,一般会通过回调 的方式减少阻塞,但是大量使用回调还会引发臭名昭著的回调地狱问题,代码的可读性、可维护性大大降低。

  • 为了增加并发度,需要引入更多的线程池,随着cpu调度的线程数量增加,导致更加严重的资源争用。同时更多的线程意味着更多的、更频繁的上下文切换 ,同时线程本身意味着占用系统资源,有限的资源被更加严重的消耗。

同步模型下,硬件资源无法被充分利用,系统吞吐量容易达到瓶颈。

NIO等异步模型

我们主要通过下面两种方式减少线程池调度开销和阻塞时间:

  • 通过RPC+NIO(例如 使用少量线程+事件驱动)异步调用方式可以降低线程数量,从而降低调度(上下文切换)开销。

  • 通过使用CompletableFuture对业务流程进行编排,降低依赖之间的阻塞。这里我们主要讲解CoumpletableFuture使用和原理

为什么选择CompletableFuture?

根据美团原文:

对于我自己而言的话,我较为熟悉的两种异步方式就是使用ThreadPoolExecutor或者回调模型,在编写项目当中发现原作者使用CompletableFuture具有以下优点:

  1. CompletableFuture使用线程池进行异步处理:性比直接创建线程的原始并发方式,线程池通过线程复用等机制显著降低线程创建销毁成本和上下文切换开销,提升系统稳定性和性能

  2. 使用CompletableFuture,对应的方法和我们手动使用线程池创建线程进行异步处理而言:我们进行异步处理更加简便,链式编排、函数式的风格更加方便使用。同时减少了业务线程阻塞,整个代码的维护性、可读性更高。(本质上是对ThreadPoolExecutor的封装,所以线程数量、上下文切换的开销并不会减少)

  3. 相比于回调模型而言,实现更简单,能够有效避免回调地狱。其异常处理更加集中、可控。

CompletableFuture使用&原理

背景&定义

解决了什么问题

Java8当中我们引入了CompletableFuture,Java8之前使用Future实现异步。

  • Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致臭名昭著的回调地狱(下面的例子会通过ListenableFuture的使用来具体进行展示)。

  • CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。

下面通过例子展示两者实现异步的差异:

假设由三个操作step1、step2、step3之间存在依赖关系,其中step3依赖step1、step2的结果。

Future(ListenableFuture)实现如下(出现回调地狱):

这里直接粘贴的美团原文,本人并不了解Future以及相关代码使用,仅是将其和CompletableFuture之间进行对比

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
ListeningExecutorService guavaExecutor = MoreExecutors.listeningDecorator(executor);
ListenableFuture<String> future1 = guavaExecutor.submit(() -> {
    //step 1
    System.out.println("执行step 1");
    return "step1 result";
});
ListenableFuture<String> future2 = guavaExecutor.submit(() -> {
    //step 2
    System.out.println("执行step 2");
    return "step2 result";
});
ListenableFuture<List<String>> future1And2 = Futures.allAsList(future1, future2);
Futures.addCallback(future1And2, new FutureCallback<List<String>>() {
    @Override
    public void onSuccess(List<String> result) {
        System.out.println(result);
        ListenableFuture<String> future3 = guavaExecutor.submit(() -> {
            System.out.println("执行step 3");
            return "step3 result";
        });
        Futures.addCallback(future3, new FutureCallback<String>() {
            @Override
            public void onSuccess(String result) {
                System.out.println(result);
            }        
            @Override
            public void onFailure(Throwable t) {
            }
        }, guavaExecutor);
    }

    @Override
    public void onFailure(Throwable t) {
    }}, guavaExecutor);

CompletableFuture实现如下:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("执行step 1");
    return "step1 result";
}, executor);
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
    System.out.println("执行step 2");
    return "step2 result";
});
cf1.thenCombine(cf2, (result1, result2) -> {
    System.out.println(result1 + " , " + result2);
    System.out.println("执行step 3");
    return "step3 result";
}).thenAccept(result3 -> System.out.println(result3));

显然CompletabelFuture的实现更加简洁,可读性也更好。

CompletableFuture定义

CompletableFuture实现了两个接口:Future&CompletionStage。Future表示异步计算的结果,CompletionStage用于表示异步执行过程当中的一个步骤(Stage),这个步骤可能是由于另外一个CompletionStage触发的,随着步骤的完成也可能触发其他一系列的CompletionStage的执行。我们根据实际业务对这些业务进行编排之后通过CompletionStage提供的thenApply、thenCompose等函数式编程方法组合编排这些步骤。

使用

使用CompletableFuture也是构建依赖树的过程。

如图,一个CompletableFuture的执行可能触发另外一系列CompletableFuture执行:

如图,描述的是一个业务接口的流程,其中包括CF1/2/3/4/5共5个步骤,并描绘了这些步骤之间的依赖关系。这5步可能是RPC调用/数据库操作/本地方法调用等一系列操作,使用CompletableFuture进行异步化编程时,途中每步骤都会产生一个CompletableFuture对象,最终结果也会是一个CompletableFuture表示。

根据CompletableFuture依赖数量,我们可以将使用分为以下几类:

  • 零依赖

  • 一元依赖

  • 二元依赖

  • 多元依赖

零依赖:CompletableFuture的创建

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
//1、使用runAsync或supplyAsync发起异步调用
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
  return "result1";
}, executor);
//2、CompletableFuture.completedFuture()直接创建一个已完成状态的CompletableFuture
CompletableFuture<String> cf2 = CompletableFuture.completedFuture("result2");
//3、先初始化一个未完成的CompletableFuture,然后通过complete()、completeExceptionally(),完成该CompletableFuture
CompletableFuture<String> cf = new CompletableFuture<>();
cf.complete("success");

步骤:

  1. 前置准备:根据业务创建CF使用的线程池

  2. 创建:

    1. 使用runAsync/supplyAsync(),发起异步调用

    2. 使用completedFuture()创建一个已经完成状态的CompletableFuture

    3. 直接通过new CompletableFuture()新建,之后通过complete()completeExceptionaly()(通常用于将回调方法转回CompletableFuture,之后通过CF能力进行编排)

第三种方法使用场景实例:

java 复制代码
@FunctionalInterface
public interface ThriftAsyncCall {
    void invoke() throws TException;
}
 /**
  * 该方法为美团内部rpc注册监听的封装,可以作为其他实现的参照
  * OctoThriftCallback 为thrift回调方法
  * ThriftAsyncCall 为自定义函数,用来表示一次thrift调用(定义如上)
  */
  public static <T> CompletableFuture<T> toCompletableFuture(final OctoThriftCallback<?,T> callback , ThriftAsyncCall thriftCall) {
   //新建一个未完成的CompletableFuture
   CompletableFuture<T> resultFuture = new CompletableFuture<>();
   //监听回调的完成,并且与CompletableFuture同步状态
   callback.addObserver(new OctoObserver<T>() {
       @Override
       public void onSuccess(T t) {
       //成功,触发CompletableFuture后续逻辑:thenXXX
           resultFuture.complete(t);
       }
       @Override
       public void onFailure(Throwable throwable) {
           resultFuture.completeExceptionally(throwable);
       }
   });
   if (thriftCall != null) {
       try {
           thriftCall.invoke();
       } catch (TException e) {
           resultFuture.completeExceptionally(e);
       }
   }
   return resultFuture;
  }

一元依赖

如上图红色链路所示,CF3,CF5分别依赖于CF1和CF2,这种对于单个CompletableFuture的依赖可以通过thenApply、thenAccept、thenCompose等方法来实现,代码如下所示:

java 复制代码
CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {
  //result1为CF1的结果
  //......
  return "result3";
});
CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {
  //result2为CF2的结果
  //......
  return "result5";
});

二元依赖

如上图红色链路所示,CF4同时依赖于两个CF1和CF2,这种二元依赖可以通过thenCombine等回调来实现,如下代码所示:

java 复制代码
CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {
  //result1和result2分别为cf1和cf2的结果
  return "result4";
});

多元依赖

如上图红色链路所示,整个流程的结束依赖于三个步骤CF3、CF4、CF5,这种多元依赖可以通过allOfanyOf方法来实现,区别是当需要多个依赖全部完成时使用allOf,当多个依赖中的任意一个完成即可时使用anyOf,如下代码所示:

java 复制代码
CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
  //这里的join并不会阻塞,因为传给thenApply的函数是在CF3、CF4、CF5全部完成时,才会执行 。
  result3 = cf3.join();
  result4 = cf4.join();
  result5 = cf5.join();
  //根据result3、result4、result5组装最终result;
  return "result";
});

实战

在我最近编写的优惠券管理系统的当中,就是用了CompletableFuture异步执行替代串行执行,用于优惠券的批量处理当中。

这里代码使用的是 知识星球 拿个offer的项目 牛券

原作者:马丁

这里仅作示范用途

并行单线程版本:

java 复制代码
goodsEmptyList.forEach(each -> {
    JSONObject jsonObject = JSON.parseObject(each.getConsumeRule());
    QueryCouponsDetailRespDTO resultQueryCouponDetail = BeanUtil.toBean(each, QueryCouponsDetailRespDTO.class);
    BigDecimal maximumDiscountAmount = jsonObject.getBigDecimal("maximumDiscountAmount");
    switch (each.getType()) {
        case 0://立减券
            resultQueryCouponDetail.setCouponAmount(maximumDiscountAmount);
            availableCouponList.add(resultQueryCouponDetail);
            break;
        case 1://满减券
            //当前金额大于指定金额
            if (requestParam.getOrderAmount().compareTo(jsonObject.getBigDecimal("termOfUse")) >= 0) {
                resultQueryCouponDetail.setCouponAmount(maximumDiscountAmount);
                availableCouponList.add(resultQueryCouponDetail);
            } else {
                notAvailableCouponList.add(resultQueryCouponDetail);
            }
            break;
        case 2://折扣券
            if (requestParam.getOrderAmount().compareTo(jsonObject.getBigDecimal("termsOfUse")) >= 0) {
                BigDecimal multiply = requestParam.getOrderAmount().multiply(jsonObject.getBigDecimal("discountRate"));
                if (multiply.compareTo(maximumDiscountAmount) >= 0) {
                    resultQueryCouponDetail.setCouponAmount(maximumDiscountAmount);
                } else {
                    resultQueryCouponDetail.setCouponAmount(multiply);
                }
                availableCouponList.add(resultQueryCouponDetail);
            } else {
                notAvailableCouponList.add(resultQueryCouponDetail);
            }
            break;
        default:
            throw new ClientException("无效的优惠券类型");
    }

});
/*
参数1:提取Map的key
参数2:提取Map的value,这里使用的是Java8接口的静态方法,"把整个对象作为map元素的value"
参数3:冲突处理策略:出现两个相同的对象,触发冲突处理策略,这里就是返回existing
 */
Map<String, QueryCouponGoodsReqDTO> goodsReqestMap = requestParam.getGoodsList().stream()
        .collect(Collectors.toMap(QueryCouponGoodsReqDTO::getGoodsNumber, Function.identity(), (existing, replacement) -> existing));
goodsNotEmptyList.forEach(each -> {
    //当前订单当中没有对应的商品
    QueryCouponGoodsReqDTO couponGoods = goodsReqestMap.get(each.getGoods());
    if (couponGoods == null) {
        notAvailableCouponList.add(BeanUtil.toBean(each, QueryCouponsDetailRespDTO.class));
    }
    JSONObject jsonObject = JSON.parseObject(each.getConsumeRule());
    QueryCouponsDetailRespDTO resultQueryCouponDetail = BeanUtil.toBean(each, QueryCouponsDetailRespDTO.class);
    switch (each.getType()) {
        case 0: // 立减券
            resultQueryCouponDetail.setCouponAmount(jsonObject.getBigDecimal("maximumDiscountAmount"));
            availableCouponList.add(resultQueryCouponDetail);
            break;
        case 1: // 满减券
            // goodsAmount 大于或等于 termsOfUse
            if (couponGoods.getGoodsAmount().compareTo(jsonObject.getBigDecimal("termsOfUse")) >= 0) {
                resultQueryCouponDetail.setCouponAmount(jsonObject.getBigDecimal("maximumDiscountAmount"));
                availableCouponList.add(resultQueryCouponDetail);
            } else {
                notAvailableCouponList.add(resultQueryCouponDetail);
            }
            break;
        case 2: // 折扣券
            // goodsAmount 大于或等于 termsOfUse
            if (couponGoods.getGoodsAmount().compareTo(jsonObject.getBigDecimal("termsOfUse")) >= 0) {
                BigDecimal discountRate = jsonObject.getBigDecimal("discountRate");
                resultQueryCouponDetail.setCouponAmount(couponGoods.getGoodsAmount().multiply(discountRate));
                availableCouponList.add(resultQueryCouponDetail);
            } else {
                notAvailableCouponList.add(resultQueryCouponDetail);
            }
            break;
        default:
            throw new ClientException("无效的优惠券类型");
    }
});

//按最终的优惠力度从大到小进行排序
availableCouponList.sort((c1, c2) -> c2.getCouponAmount().compareTo(c1.getCouponAmount()));

使用CF的多线程异步版本

java 复制代码
//并行处理goodsEmptyList、goodsNotEmptyList当中每一个元素
        CompletableFuture<Void> emptyGoodsTasks = CompletableFuture.allOf(
                goodsEmptyList.stream()
                        //runAsync开启异步任务,通过线程池并行执行
                        .map(each -> CompletableFuture.runAsync(() -> {
                            //任务体
                            QueryCouponsDetailRespDTO resultCouponDetail = BeanUtil.toBean(each, QueryCouponsDetailRespDTO.class);
                            JSONObject jsonObject = JSON.parseObject(each.getConsumeRule());
                            handleCouponLogic(resultCouponDetail, jsonObject, requestParam.getOrderAmount(), availableCouponList, notAvailableCouponList);
                        }, executorService))
                        //整合为大的任务,将零三润物初次整合为一个任务组
                        .toArray(CompletableFuture[]::new)
        );

        Map<String, QueryCouponGoodsReqDTO> goodsRequestMap = requestParam.getGoodsList().stream()
                .collect(Collectors.toMap(QueryCouponGoodsReqDTO::getGoodsNumber, Function.identity()));

        CompletableFuture<Void> notEmptyGoodsTasks = CompletableFuture.allOf(
                goodsNotEmptyList.stream()
                        .map(each -> CompletableFuture.runAsync(() -> {
                            QueryCouponsDetailRespDTO resultCouponDetail = BeanUtil.toBean(each, QueryCouponsDetailRespDTO.class);
                            QueryCouponGoodsReqDTO couponGoods = goodsRequestMap.get(each.getGoods());
                            if (couponGoods == null) {
                                notAvailableCouponList.add(resultCouponDetail);
                            } else {
                                JSONObject jsonObject = JSON.parseObject(each.getConsumeRule());
                                handleCouponLogic(resultCouponDetail, jsonObject, couponGoods.getGoodsAmount(), availableCouponList, notAvailableCouponList);
                            }
                        }, executorService))
                        .toArray(CompletableFuture[]::new)
        );

        //等待亮哥异步任务集合完成
        //两个任务组整合为大任务组
        CompletableFuture.allOf(emptyGoodsTasks,notEmptyGoodsTasks)
                .thenRun(()->{
                    //执行完成之后进行排序(后置处理)
                    availableCouponList.sort((c1, c2) -> c2.getCouponAmount().compareTo(c1.getCouponAmount()));
                })
                //阻塞等待(因为任务是通过线程池异步执行的
                .join();
java 复制代码
//优惠券判断逻辑,根据条件判断放入可用、不可用列表
    private void handleCouponLogic(QueryCouponsDetailRespDTO resultCouponDetail, JSONObject jsonObject, BigDecimal amount,
                                   List<QueryCouponsDetailRespDTO> availableCouponList, List<QueryCouponsDetailRespDTO> notAvailableCouponList) {
        BigDecimal termOfUse = jsonObject.getBigDecimal("termOfUse");
        BigDecimal maximumDiscountAmount = jsonObject.getBigDecimal("maximumDiscountAmount");

        switch (resultCouponDetail.getType()) {
            case 0://立减券
                resultCouponDetail.setCouponAmount(maximumDiscountAmount);
                availableCouponList.add(resultCouponDetail);
                break;
            case 1://满减券
                if (amount.compareTo(termOfUse) >= 0) {
                    resultCouponDetail.setCouponAmount(maximumDiscountAmount);
                    availableCouponList.add(resultCouponDetail);
                } else notAvailableCouponList.add(resultCouponDetail);
                break;
            case 2://折扣券
                if (amount.compareTo(termOfUse) >= 0) {
                    BigDecimal discountRate = jsonObject.getBigDecimal("discountRate");
                    BigDecimal multiply = amount.multiply(discountRate);
                    if (multiply.compareTo(maximumDiscountAmount) >= 0) {
                        resultCouponDetail.setCouponAmount(maximumDiscountAmount);
                    } else resultCouponDetail.setCouponAmount(multiply);
                    availableCouponList.add(resultCouponDetail);
                } else notAvailableCouponList.add(resultCouponDetail);
                break;
            default:
                throw new ClientException("无效的优惠券类型");
        }

    }

构建思路:

  1. 构建单个优惠券执行的CF对象,通过runAsync将优惠券的处理逻辑交给线程池,实现单个优惠券的异步处理

  2. 将所有的优惠券通过两层共三个allOf()方法进行聚合

  3. 通过join阻塞等待最终聚合的CF完成,确保全部优惠券并行处理执行完毕,最终的allOf后置方法执行thenRun()当中的排序

原理

CompletableFuture当中包含两个字段:resultstack。result存储当前CF结果,stack表示当前CF需要触发的依赖动作,触发依赖它的CF计算。依赖动作可以有多个(意味着有多个依赖它的CF),通过栈的形式存储,stack表示栈顶元素。

CF基本结构:

依赖动作都封装在单独的COmpletion子类当中,如下面关系图所示。CompletableFuture当中而米格方法都对应一个Completion子类。

  • UniCompletion继承了Completion,是一元依赖的基类,例如thenApply的实现类UniApply就继承自UniCompletion。

  • BiCompletion继承了UniCompletion,是二元依赖的基类,同时也是多元依赖的基类。例如thenCombine的实现类BiRelay就继承自BiCompletion。

设计思想

按照类似观察者模式的设计思想,我们可以从观察者 和被观察者两方面入手分析原理。因为回调种类多但是结果差异并不大,这里仅分析一元依赖thenApply。

被观察者
  1. 每个CF都可以被看成是被观察者,内部由一个Completion类型的链表stack,用于存储注册到其中的所有观察者。被观察者执行完后弹栈stack属性,依次通知注册到其中的观察者。例如:上图当中的fn2作为观察者封装在UniApply当中。

  2. 被观察者的result属性用来存储返回结果的数据。上图中对应CF1的Functon fn1执行结果。

观察者

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

  1. 观察者中的dep属性:指向其对应的CompletableFuture,在上面的例子中dep指向CF2。

  2. 观察者中的src属性:指向其依赖的CompletableFuture,在上面的例子中src指向CF1。

  3. 观察者Completion中的fn属性:用来存储具体的等待被回调的函数。这里需要注意的是不同的回调方法(thenAccept、thenApply、exceptionally等)接收的函数类型也不同,即fn的类型有很多种,在上面的例子中fn指向fn2。

整体流程

一元依赖

使用thenApply讲解流程:

  1. 观察者Completion到CF1,此时CF1将观察者Completion压栈。

  2. 当CF1操作完成时,将结果赋值给CF1当中属性result

  3. 依次弹栈,通知观察者尝试运行

并发问题&思考

Q1 :在观察者注册之前,如果CF已经执行完成,并且已经发出通知,那么这时观察者由于错过了通知是不是将永远不会被触发呢 ? A1:不会。在注册时检查依赖的CF是否已经完成。如果未完成(即result == null)则将观察者入栈,如果已完成(result != null)则直接触发观察者操作。

Q2:在"入栈"前会有"result == null"的判断,这两个操作为非原子操作,CompletableFufure的实现也没有对两个操作进行加锁,完成时间在这两个操作之间,观察者仍然得不到通知,是不是仍然无法触发?

A2:不会。入栈之后再次检查CF是否完成,如果完成则触发。

Q3:当依赖多个CF时,观察者会被压入所有依赖的CF的栈中,每个CF完成的时候都会进行,那么会不会导致一个操作被多次执行呢 ?如下图所示,即当CF1、CF2同时完成时,如何避免CF3被多次触发。

A3:CompletableFuture的实现是这样解决该问题的:观察者在执行之前会先通过CAS(乐观锁的Compare and Swap机制)操作设置一个状态位,将status由0改为1。如果观察者已经执行过了,那么CAS操作将会失败,取消执行。

可以看到CompletionFuture本身在处理并行问题时,全程没有加锁,极大提高程序执行效率。

将并行问题考虑纳入之后,整体流程图:

所有的回调模型复用同一套六层架构,不同回调监听通过策略模式实现差异化。

二元

使用thenCombine为例:

thenCombine:依赖两个CF。观察者实现类BiApply,通过src和snd两个属性关联被依赖的CF,fn属性为BiFunction。

和单个不同的是,在依赖CF未完成之前,thenCombine会尝试将BiApply压入这两个被依赖的CF栈当中,每个被依赖CF完成时都会尝试触发被观察者BiApply,BiApply会检查连哥哥以来是否都完成,两个依赖都完成之后开始执行(通过CAS验证避免重复触发)。

多元

依赖多个CF的回调方法包括allOf anyOf ,区别在于allOf观察者实现类BiRelay需要所有依赖 完成之后执行回调;而anyOf实现类OrRelay,任意一个被依赖CF完成之后被触发。两者实现方式都是将多个被依赖的CF构建为一个平衡二叉树,执行结果层层通知,直到根节点触发回调监听。

实践总结

代码执行线程

要合理治理线程资源,最基本的前提条件就是要写代码时清楚知道代码执行的线程。

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

同步方法(不使用Async后缀):

  1. 注册时被依赖的操作已经完成,直接通过当前线程执行。

  2. 注册时被依赖操作没有执行完成,则由回调线程执行。

异步方法:

可以选择是否传递线程池参数Executor运行在指定线程池当中;当不传递Executor时,会使用ForkJoinPool中的公用现场给你吃CommonPool(注意:CommonPool大小(指的是ForkJoinPool的parallelism也就是期望同时并行执行任务的线程数量,实际线程数量可能超过(线程发生阻塞时创建额外线程防止并行度下降))是CPU核心数量-1,IO密集型应用线程数可能成为瓶颈)

java 复制代码
ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("supplyAsync 执行线程:" + Thread.currentThread().getName());
    //业务操作
    return "";
}, threadPool1);
//此时,如果future1中的业务操作已经执行完毕并返回,则该thenApply直接由当前main线程执行;否则,将会由执行以上业务操作的threadPool1中的线程执行。
future1.thenApply(value -> {
    System.out.println("thenApply 执行线程:" + Thread.currentThread().getName());
、    return value + "1";
});
//使用ForkJoinPool中的共用线程池CommonPool
future1.thenApplyAsync(value -> {
//do something
  return value + "1";
});
//使用指定线程池
future1.thenApplyAsync(value -> {
//do something
  return value + "1";
}, threadPool1);

线程池须知

异步回调需要传线程池

上面提到,异步回调方法可以选择是否传毒线程池参数executor,美团科技团队建议强制传线程池,并根据实际情况做线程池隔离。

如果不传入线程池就会使用公共线程池(上文说的CommonPool),所有调用都将共用这个线程池,那么由于核心线程数=处理器数量-1(单线程为1),所有业务无论核心与否都将竞争这同一个池当中所有线程,容易造成系统瓶颈。手动传入线程池,我们不仅可以根据需求创建符合要求的线程池,提高业务性能,还能给不同业务分配不同的线程池,做到资源隔离,减少业务间的相互干扰。同时,我们还能手动调整线程池各项参数。

线程池循环引用会导致死锁

实例:

java 复制代码
public Object doGet() {
  ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
  CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> {
  //do sth
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("child");
        return "child";
      }, threadPool1).join();//子任务
    }, threadPool1);
  return cf1.join();
}

上面代码当中,doGet方法第三行当中通过supplyAsync向线程池threadPool1请求线程,并且欸不子任务又向threadPool1请求线程。threadPool核心线程为10个,最大线程数量也是10。

如果出现同一个时刻有10个请求道道,则threadPool1被占满,子任务请求线程时这里进入阻塞队列进行排队,而父任务的完成又依赖于子而南无,这时由于子任务得不到线程,父任务一直无法完成。主线程执行又使用了join阻塞式等待所有任务完成之后进行返回,就卡死了。

修复:将父任务和子任务的线程池进行隔离,两个任务请求不同的线程池,避免循环依赖导致阻塞。

异步RPC调用注意不要/减少使用阻塞IO线程池

服务异步化后很多步骤都会依赖于异步RPC调用的结果,这时需要特别注意一点,如果是使用基于NIO(比如Netty)的异步RPC,则返回结果是由IO线程负责设置的,即回调方法由IO线程触发,CompletableFuture同步回调(如thenApply、thenAccept等无Async后缀的方法)如果依赖的异步RPC调用的返回结果,那么这些同步回调将运行在IO线程上,而整个服务只有一个IO线程池,这时需要保证同步回调中不能有阻塞等耗时过长的逻辑,否则在这些逻辑执行完成前,IO线程将一直被占用,影响整个服务的响应。

异常处理

由于异步执行的任务是在其他线程上执行的,而异常信息存储在信息栈当中,因此当前线程除非阻塞等待返回结果,否则无法通过传统的try/catch捕获到异常。CF本身提供了异常捕获回调exceptionally,相当于同步的try/catch

示例代码:

java 复制代码
@Autowired
private WmOrderAdditionInfoThriftService wmOrderAdditionInfoThriftService;//内部接口
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
    CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
    return remarkResultFuture
      .exceptionally(err -> {//通过exceptionally 捕获异常,打印日志并返回默认值
         log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, err);
         return 0;
      });
}

注意:

CF的异常回调会对异常进行包装。大部分被包装为CompletionException抛出,真正异常存储在cause属性(返回 导致当前异常根源异常,一般而言存在两种情况:1.为null 2.存在异常链,不为null)当中,如果想要真正处理对应异常需要通过Throwable.getCause()提取真正的异常,但是也存在返回真正异常的情况。

推荐使用工具类提取异常之后进行处理、

自定义异常工具类,提取异常:

java 复制代码
public class ExceptionUtils {
    public static Throwable extractRealException(Throwable throwable) {
          //这里判断异常类型是否为CompletionException、ExecutionException,如果是则进行提取,否则直接返回。
        if (throwable instanceof CompletionException || throwable instanceof ExecutionException) {
            if (throwable.getCause() != null) {
                return throwable.getCause();
            }
        }
        return throwable;
    }
}

使用工具类处理示范:

java 复制代码
@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;
      });
}
相关推荐
heartbeat..5 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
lxl13076 小时前
学习C++(5)运算符重载+赋值运算符重载
学习
6 小时前
java关于内部类
java·开发语言
好好沉淀6 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin6 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder6 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~6 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟6 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日6 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水6 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展