当前位置: 首页 > news >正文

CompletableFuture超详解与实践

0.背景

一个接口可能需要调用 N 个其他服务的接口,这在项目开发中还是挺常见的。举个例子:用户请求获取订单信息,可能需要调用用户信息、商品详情、物流信息、商品推荐等接口,最后再汇总数据统一返回。

如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速度会非常慢。考虑到这些接口之间有大部分都是 无前后顺序关联 的,可以 并行执行 ,就比如说调用获取商品详情的时候,可以同时调用获取物流信息。通过并行执行多个任务的方式,接口的响应速度会得到大幅优化。

serial-to-parallel

对于存在前后顺序关系的接口调用,可以进行编排,如下图所示。

img

在外卖商家端交易业务存在这样的特点:

1.服务端必须一次返回订单卡片所有内容:根据商家端和服务端的“增量同步协 议注 1”,服务端必须一次性返回订单的所有信息,包含订单主信息、商品、结算、配送、用户信息、骑手信息、餐损、退款、客服赔付(参照下面订单卡片截图)等,需要从下游三十多个服务中获取数据。在特定条件下,如第一次登录和长时间没登录的情况下,客户端会分页拉取多个订单,这样发起的远程调用会更多。

2.商家端和服务端交互频繁:商家对订单状态变化敏感,多种推拉机制保证每次变更能够触达商家,导致 App 和服务端的交互频繁,每次变更需要拉取订单最新的全部内容在外卖交易链路如此大的流量下,为了保证商家的用户体验,保证接口的高性能,并行从下游获取数据就成为必然

综上所述,在多模块大流量的场景下原先的串行接口的响应是行不通的,所以并行接口的响应是必然的

1.CompletableFuture使用与原理

1.1Future介绍

Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。

这其实就是多线程中经典的 Future 模式,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。

在 Java 中,Future 类只是一个泛型接口,位于 java.util.concurrent 包下,其中定义了 5 个方法,主要包括下面这 4 个功能:

  • 取消任务;
  • 判断任务是否被取消;
  • 判断任务是否已经执行完成;
  • 获取任务执行结果。
// V 代表了Future执行的任务返回值的类型
public interface Future<V> {// 取消任务执行// 成功取消返回 true,否则返回 falseboolean cancel(boolean mayInterruptIfRunning);// 判断任务是否被取消boolean isCancelled();// 判断任务是否已经执行完成boolean isDone();// 获取任务执行结果V get() throws InterruptedException, ExecutionException;// 指定时间内没有返回计算结果就抛出 TimeOutException 异常V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutExceptio}
//举例
public class FutureTest {public static void main(String[] args) throws InterruptedException, ExecutionException {ExecutorService executorService = Executors.newFixedThreadPool(5);UserInfoService userInfoService = new UserInfoService();MedalService medalService = new MedalService();long userId = 666L;long startTime = System.currentTimeMillis();//调用用户服务获取用户基本信息FutureTask<UserInfo> userInfoFutureTask = new FutureTask<>(new Callable<UserInfo>() {@Overridepublic UserInfo call() throws Exception {return userInfoService.getUserInfo(userId);}});executorService.submit(userInfoFutureTask);Thread.sleep(300); //模拟主线程其它操作耗时FutureTask<MedalInfo> medalInfoFutureTask = new FutureTask<>(new Callable<MedalInfo>() {@Overridepublic MedalInfo call() throws Exception {return medalService.getMedalInfo(userId);}});executorService.submit(medalInfoFutureTask);UserInfo userInfo = userInfoFutureTask.get();//获取个人信息结果MedalInfo medalInfo = medalInfoFutureTask.get();//获取勋章信息结果System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");}
}class UserInfoService {public UserInfo getUserInfo(Long userId) throws InterruptedException {Thread.sleep(300);//模拟调用耗时return new UserInfo("666", "捡田螺的小男孩", 27); //一般是查数据库,或者远程调用返回的}}class MedalService {public MedalInfo getMedalInfo(long userId) throws InterruptedException {Thread.sleep(500); //模拟调用耗时return new MedalInfo("666", "守护勋章");}
}

如果我们不使用Future进行并行异步调用,而是在主线程串行进行的话,耗时大约为300+500+300 = 1100 ms。可以发现,future+线程池异步配合,提高了程序的执行效率。

但是Future对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。

  • Future.get() 就是阻塞调用,在线程获取结果之前get方法会一直阻塞
  • Future提供了一个isDone方法,可以在程序中轮询这个方法查询执行结果。

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

1.2CompletableFuture的背景和定义

1.2.1CompletableFuture解决的问题

CompletableFuture 是由 Java 8 引入的,在 Java8 之前我们一般通过 Future 实现 异步。

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

● CompletableFuture 对 Future 进行了扩展,可以通过设置回调的方式处理计

算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。

下面将举例来说明,我们通过 ListenableFuture、CompletableFuture 来实现异步 的差异。假设有三个操作 step1、step2、step3 存在依赖关系,其中 step3 的执行依赖 step1 和 step2 的结果。

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

 ExecutorService executor = Executors.newFixedThreadPool(5);ListeningExecutorService guavaExecutor = MoreExecutors.listeningDecorator(executor);ListenableFuture<String> future1 = guavaExecutor.submit(() -> {//step 1System.out.println(“执行 step 1);return“step1 result”;});ListenableFuture<String> future2 = guavaExecutor.submit(() -> {//step 2System.out.println(“执行 step 2);return“step2 result”;});ListenableFuture<List<String>> future1And2 = Futures.allAsList(future1,future2);
Futures.addCallback(future1And2,new FutureCallback<List<String>>(){@Overridepublic 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>() {@Overridepublic void onSuccess(String result) {System.out.println(result);}@Overridepublic void onFailure(Throwable t) {}}, guavaExecutor);}@Overridepublic void onFailure (Throwable t){}},guavaExecutor);

CompletableFuture 的实现如下:

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));

显然,CompletableFuture 的实现更为简洁,可读性更好。

1.2.1 CompletableFuture 的定义

img

CompletableFuture 实现了两个接口(如上图所示):Future、CompletionStage。 Future 表示异步计算的结果,CompletionStage 用于表示异步执行过程中的一个步 骤(Stage),这个步骤可能是由另外一个 CompletionStage 触发的,随着当前步骤的完成,也可能会触发其他一系列 CompletionStage 的执行。从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage 接口正是定义了这样的能力,我们可以通过其提供的 thenAppythenCompose 等函数式编程方法来组合编排这些步骤。

1.3CompletableFuture 的使用

下面我们通过一个例子来讲解 CompletableFuture 如何使用,使用 CompletableFuture 也是构建依赖树的过程。一个 CompletableFuture 的完成会触发另外一系列依赖它的 CompletableFuture 的执行

在这里插入图片描述

如上图所示,这里描绘的是一个业务接口的流程,其中包括 CF1\CF2\CF3\CF4\CF5 共 5 个步骤,并描绘了这些步骤之间的依赖关系,每个步骤可以是一次 RPC 调用、一次数据库操作或者是一次本地方法调用等,在使用 CompletableFuture 进行 异步化编程时,图中的每个步骤都会产生一个 CompletableFuture 对象,最终结果

也会用一个 CompletableFuture 来进行表示。根据 CompletableFuture 依赖数量,可以分为以下几类:零依赖、一元依赖、二元依赖和多元依赖

1.3.1 零依赖:CompletableFuture 的创建

我们先看下如何不依赖其他 CompletableFuture 来创建新的 CompletableFuture:

在这里插入图片描述

如上图红色链路所示,接口接收到请求后,首先发起两个异步调用 CF1、CF2,主要有三种方式:

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

第三种方式的一个典型使用场景,就是将回调方法转为 CompletableFuture,然后再依赖 CompletableFure 的能力进行调用编排,示例如下:

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

在这里插入图片描述

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

CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {//result1 为 CF1 的结果//......return“result3”;
});
CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {//result2 为 CF2 的结果//......return“result5”;
});
1.3.3 二元依赖:依赖两个 CF

在这里插入图片描述

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

CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {//result1 和 result2 分别为 cf1 和 cf2 的结果return“result4”;
});
1.3.4 多元依赖:依赖多个 CF

在这里插入图片描述

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

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”;
});

1.4CompletableFuture 原理

CompletableFuture 中包含两个字段:resultstack。result 用于存储当前 CF的结果,stack(Completion)表示当前 CF 完成后需要触发的依赖动作(Dependency Actions),去触发依赖它的 CF 的计算,依赖动作可以有多个(表示有多个依赖它的 CF),以栈(Treiber stack)的形式存储,stack 表示栈顶元素

在这里插入图片描述

这种方式类似“观察者模式”,依赖动作(Dependency Action)都封装在一个单独Completion 子类中。下面是 Completion 类关系结构图。CompletableFuture 中的每个方法都对应了图中的一个 Completion 的子类,Completion 本身是观察者的基类。

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

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

在这里插入图片描述

1.4.1 CompletableFuture 的设计思想

按照类似“观察者模式”的设计思想,原理分析可以从“观察者”和“被观察者”两个方面着手。由于回调种类多,但结构差异不大,所以这里单以一元依赖中的thenApply 为例,不再枚举全部回调类型。如下图所示:

在这里插入图片描述

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

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。
1.4.2 整体流程
1.4.2.1 一元依赖

这里仍然以 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 操作设置一个状态位,将 status 由 0 改为 1。如果观察者已经执行过了,那么 CAS 操作将会失败,取消执行

1.4.2.2thenApply源码分析
 public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {return uniApplyStage(null, fn);}private <V> CompletableFuture<V> uniApplyStage(Executor e, Function<? super T,? extends V> f) {if (f == null) throw new NullPointerException();//1.创建一个新的CompletableFuture对象CompletableFuture<V> d =  new CompletableFuture<V>();if (e != null || !d.uniApply(this, f, null)) {//2.构建UniApplye代表线程池 d 代表新的CompletableFuture this 代表当前f 代表方法 这个时候 UniApply 内部的所有的引用都处于为null的状态UniApply<T,V> c = new UniApply<T,V>(e, d, this, f);//3. c其实就是Completion对象,被push到栈中push(c);//4. 尝试执行cc.tryFire(SYNC);}// 5. 这个d会一直返回到调用thenApply的地方,后续的链式调用会作用在这个d上面return d;}@SuppressWarnings("serial")static final class UniApply<T,V> extends UniCompletion<T,V> {Function<? super T,? extends V> fn;UniApply(Executor executor, CompletableFuture<V> dep,CompletableFuture<T> src,Function<? super T,? extends V> fn) {//2.1 向上执行super(executor, dep, src); this.fn = fn;}}abstract static class UniCompletion<T,V> extends Completion {Executor executor;                 // executor to use (null if none)CompletableFuture<V> dep;          // the dependent to completeCompletableFuture<T> src;          // source for actionUniCompletion(Executor executor, CompletableFuture<V> dep,CompletableFuture<T> src) {//2.2 dep就是新创建的d  src就是当前的thisthis.executor = executor; this.dep = dep; this.src = src;}}

关于执行第2步的时候,构建的对象如下图, src和dep都是空的CompletableFuture,next为Null,这里我们会发现所有的都是继承Completion对象,最终所有都是构建都可以理解为Completion对象;

关于执行第3步的时候,构建的UniApply对象的内容完成压栈的操作,将CompletableFuture的stack属性指向Completion对象;

接下来看第4步操作,尝试执行Completion;

 @SuppressWarnings("serial")static final class UniApply<T,V> extends UniCompletion<T,V> {Function<? super T,? extends V> fn;UniApply(Executor executor, CompletableFuture<V> dep,CompletableFuture<T> src,Function<? super T,? extends V> fn) {super(executor, dep, src); this.fn = fn;}final CompletableFuture<V> tryFire(int mode) {//4.1 d新创建的 a(也是c中的src) 就是原来的CompletableFuture<V> d; CompletableFuture<T> a;//4.2 如果uniApply执行成功,则会进到下面的postFire调用否则返回null 如果返回null,就要等待以后的主动complete来再次触发if ((d = dep) == null ||!d.uniApply(a = src, fn, mode > 0 ? null : this))return null;//4.5 tryFire成功后,会把以下几个属性设为null,表面此Completion已经完成任务,变成dead状态dep = null; src = null; fn = null;//4.6 出栈return d.postFire(a, mode);}}final <S> boolean uniApply(CompletableFuture<S> a,Function<? super S,? extends T> f,UniApply<S,T> c) {Object r; Throwable x;//4.3 如果a(也是c中的src)没有准备完成,那result是空,这里就会直接返回falseif (a == null || (r = a.result) == null || f == null)return false;tryComplete: if (result == null) {if (r instanceof AltResult) {if ((x = ((AltResult)r).ex) != null) {completeThrowable(x, r);break tryComplete;}r = null;}try {if (c != null && !c.claim())return false;@SuppressWarnings("unchecked") S s = (S) r;//4.4 如果r不为空,则会作为f的输入参数,f的输出则成为当前CompletableFuture的完成值completeValue(f.apply(s));} catch (Throwable ex) {completeThrowable(ex);}}return true;}

第5步返回d, 这个d会返回到调用thenApply的地方,后续的链式调用会作用在这个d上面,接下来我们可以看到base对象就是我们构建好的第一个链;

这里我们可以猜测后续的执行thenApply的方法,也就是执行完成test1的第二行代码,生成的结构如下图

在这里插入图片描述

当我们的代码执行到test1的第3行的时候,也就是complete方法,该方法也就是为了解决我们执行tryFire执行失败后动作,源码如下:

public boolean complete(T value) {boolean triggered = completeValue(value);postComplete();return triggered;}final void postComplete() {//1. this表示当前的CompletableFuture, 也就是我们baseCompletableFuture<?> f = this; Completion h;//2. 判断stack是否为空  或者如果f的栈为空且不是this则重置while ((h = f.stack) != null ||(f != this && (h = (f = this).stack) != null)) {CompletableFuture<?> d; Completion t;//3. CAS出栈if (f.casStack(h, t = h.next)) {if (t != null) { //4.出栈的h不是最后一个元素,最后一个元素直接执行7即可if (f != this) {//5. 如果f不是this,将刚出栈的h, 入this的栈顶//我猜测这个地方大家会有迷惑pushStack(h);continue;}h.next = null;    //6. detach}f = (d = h.tryFire(NESTED)) == null ? this : d;  //7.调用tryFire}}}
1.4.2.3 二元依赖

我们以 thenCombine 为例来说明二元依赖:

在这里插入图片描述

thenCombine 操作表示依赖两个 CompletableFuture。其观察者实现类为 BiApply,
如上图所示,BiApply 通过 src 和 snd 两个属性关联被依赖的两个 CF,fn 属性的类型为 BiFunction。与单个依赖不同的是,在依赖的 CF 未完成的情况下,thenCombine 会尝试将 BiApply 压入这两个被依赖的 CF 的栈中,每个被依赖的 CF 完成时都会尝试触发观察者 BiApply,BiApply 会检查两个依赖是否都完成,如果完成则开始执行。这里为了解决重复触发的问题,同样用的是上一章节提到的 CAS 操作,执行时会先通过 CAS 设置状态位,避免重复触发。

1.4.2.4 多元依赖

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

在这里插入图片描述

1.4.3 小结

本章节为 CompletableFuture 实现原理的科普,旨在尝试不粘贴源码,而通过结构图、流程图以及搭配文字描述把 CompletableFuture 的实现原理讲述清楚。把晦 涩的源码翻译为“整体流程”章节的流程图,并且将并发处理的逻辑融入,便于大家理解

2.实践总结

2.1线程阻塞问题:

2.1.1 代码执行在哪个线程上?

要合理治理线程资源,最基本的前提条件就是要在写代码时,清楚地知道每一行代码都将执行在哪个线程上。下面我们看一下 CompletableFuture 的执行线程情况。CompletableFuture 实现了 CompletionStage 接口,通过丰富的回调方法,支持各种组合操作,每种组合场景都有同步和异步两种方法。
同步方法(即不带 Async 后缀的方法)有两种情况。

● 如果注册时被依赖的操作已经执行完成,则直接由当前线程执行。

● 如果注册时被依赖的操作还未执行完,则由回调线程执行

异步方法(即带 Async 后缀的方法):可以选择是否传递线程池参数 Executor 运行在指定线程池中;当不传递 Executor 时,会使用 ForkJoinPool 中的共用线程池CommonPool(CommonPool 的大小是 CPU 核数 -1,如果是 IO 密集的应用,线程数可能成为瓶颈)。

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 somethingreturn value +1;});
// 使用指定线程池
future1.thenApplyAsync(value -> {
//do somethingreturn value +1;
}, threadPool1);
2.1.2 异步回调要传线程池

前面提到,异步回调方法可以选择是否传递线程池参数 Executor,这里我们建议强制传线程池,且根据实际情况做线程池隔离。当不传递线程池时,会使用 ForkJoinPool 中的公共线程池 CommonPool,这里所有调用将共用该线程池,核心线程数 = 处理器数量 -1(单核核心线程数为 1),所有异步回调都会共用该 CommonPool,核心与非核心业务都竞争同一个池中的线程,很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的相互干扰。

2.1.3 线程池循环引用会导致死锁
public Object doGet() {ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, 
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> {//do sthreturn CompletableFuture.supplyAsync(() -> {System.out.println(“child”);return“child”;}, threadPool1).join();// 子任务}, threadPool1);return cf1.join();
}

如上代码块所示,doGet 方法第三行通过 supplyAsync 向 threadPool1 请求线程,并且内部子任务又向 threadPool1 请求线程。threadPool1 大小为 10,当同一时刻有 10 个请求到达,则 threadPool1 被打满,子任务请求线程时进入阻塞队列排队,但是父任务的完成又依赖于子任务,这时由于子任务得不到线程,父任务无法完成。

主线程执行 cf1.join() 进入阻塞状态,并且永远无法恢复。为了修复该问题,需要将父任务与子任务做线程池隔离,两个任务请求不同的线程池,避免循环依赖导致的阻塞

2.1.4 异步 RPC 调用注意不要阻塞 IO 线程池

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

2.2 其他

2.2.1 异常处理

由于异步执行的任务在其他线程上执行,而异常信息存储在线程栈中,因此当前线程除非阻塞等待返回结果,否则无法通过 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.exceptionally(err -> {// 通过 exceptionally 捕获异常,打印日志并返回默认值log.error(WmOrderRemarkService.getCancelTypeAsync Exception 
orderId={}, orderId, err);
return 0;});
}

3.异步化收益

通过异步化改造,API 系统的性能得到明显提升,与改造前对比的收益

如下:

● 核心接口吞吐量大幅提升,其中订单轮询接口改造前 TP99 为 754ms,改造后降为 408ms。

● 服务器数量减少 1/3。
参考文献:
《2022年美团技术年货-合辑》

相关文章:

CompletableFuture超详解与实践

0.背景 一个接口可能需要调用 N 个其他服务的接口&#xff0c;这在项目开发中还是挺常见的。举个例子&#xff1a;用户请求获取订单信息&#xff0c;可能需要调用用户信息、商品详情、物流信息、商品推荐等接口&#xff0c;最后再汇总数据统一返回。 如果是串行&#xff08;按…...

Maven之私服

1 介绍 团队开发现状分析私服是一台独立的服务器&#xff0c;用于解决团队内部的资源共享与资源同步问题Nexus Sonatype公司的一款maven私服产品 下载地址&#xff1a;https://help.sonatype.com/repomanager3/download win版安装包&#xff1a;https://pan.baidu.com/s/1wk…...

#define宏定义的初探

前言&#xff1a; 最基本的#define定义方式 #define可以定义宏&#xff0c;这点相信大家并不陌生&#xff0c;其定义的方式十分简单&#xff0c;给大家随便来一个最简单、最基础的定义方式看看&#xff1a; #include<stdio.h> #define a 3 int main() { printf(&quo…...

机器学习 -决策树的案例

场景 我们对决策树的基本概念和算法其实已经有过了解&#xff0c;那我们如何利用决策树解决问题呢&#xff1f; 构建决策树 数据准备 我们准备了一些数据如下&#xff1a; # 定义新的数据集 new_dataSet [[晴朗, 是, 高, 是],[雨天, 否, 低, 否],[阴天, 是, 中, 是],[晴朗…...

04、Kafka ------ 各个功能的作用解释(Cluster、集群、Broker、位移主题、复制因子、领导者副本、主题)

目录 启动命令&#xff1a;CMAK的用法★ 在CMAK中添加 Cluster★ 在CMAK中查看指定集群★ 在CMAK中查看 Broker★ 位移主题★ 复制因子★ 领导者副本和追随者副本★ 查看主题 启动命令&#xff1a; 1、启动 zookeeper 服务器端 小黑窗输入命令&#xff1a; zkServer 2、启动 …...

1、C语言:数据类型/运算符与表达式

数据类型/运算符/表达式 1.数据类型与长度2.常量3.声明4. 运算符5. 表达式 1.数据类型与长度 基本数据类型 类型说明char字符型&#xff0c;占用一个字节&#xff0c;可以存放本地字符集中的一个字符int整型&#xff0c;通常反映了所有机器中整数的最自然长度float单精度浮点…...

[ffmpeg系列 03] 文件、流地址(视频)解码为YUV

一 代码 ffmpeg版本5.1.2&#xff0c;dll是&#xff1a;ffmpeg-5.1.2-full_build-shared。x64的。 文件、流地址对使用者来说是一样。 流地址(RTMP、HTTP-FLV、RTSP等)&#xff1a;信令完成后&#xff0c;才进行音视频传输。信令包括音视频格式、参数等协商。 接流的在实际…...

python算法每日一练:连续子数组的最大和

这是一道关于动态规划的算法题&#xff1a; 题目描述&#xff1a; 给定一个整数数组 nums&#xff0c;请找出该数组中连续子数组的最大和&#xff0c;并返回这个最大和。 示例&#xff1a; 输入&#xff1a;[-2, 1, -3, 4, -1, 2, 1, -5, 4] 输出&#xff1a;6 解释&#xff…...

一个vue3的tree组件

https://download.csdn.net/download/weixin_41012767/88709466...

新手练习项目 4:简易2048游戏的实现(C++)

名人说&#xff1a;莫听穿林打叶声&#xff0c;何妨吟啸且徐行。—— 苏轼《定风波莫听穿林打叶声》 Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#xff09; 目录 一、效果图二、代码&#xff08;带注释&#xff09;三、说明 一、效果图 二、代码&#xff08;带…...

2023年度总结:技术沉淀、持续学习

2023年度总结&#xff1a;技术沉淀、持续学习 一、引言 今年是我毕业的第二个年头&#xff0c;也是完整的一年&#xff0c;到了做年终总结的时候了 这一年谈了女朋友&#xff0c;学习了不少技术&#xff0c;是充实且美好的一年&#xff01; 首先先看年初定的小目标&#xf…...

Unity 利用UGUI之Slider制作进度条

在Unity中使用Slider和Text组件可以制作简单的进度条。 首先在场景中右键->UI->Slider&#xff0c;新建一个Slider组件&#xff1a; 同样方法新建一个Text组件&#xff0c;最终如图&#xff1a; 创建一个进度模拟脚本&#xff0c;Slider_Progressbar.cs using System.C…...

OCS2 入门教程(四)- 机器人示例

系列文章目录 前言 OCS2 包含多个机器人示例。我们在此简要讨论每个示例的主要特点。 System State Dim. Input Dim. Constrained Caching Double Integrator 2 1 No No Cartpole 4 1 Yes No Ballbot 10 3 No No Quadrotor 12 4 No No Mobile Manipul…...

FreeRTOS学习第6篇–任务状态挂起恢复删除等操作

目录 FreeRTOS学习第6篇--任务状态挂起恢复删除等操作任务的状态设计实验IRReceiver_Task任务相关代码片段实验现象本文中使用的测试工程 FreeRTOS学习第6篇–任务状态挂起恢复删除等操作 本文目标&#xff1a;学习与使用FreeRTOS中的几项操作&#xff0c;有挂起恢复删除等操作…...

BLE Mesh蓝牙组网技术详细解析之Access Layer访问层(六)

目录 一、什么是BLE Mesh Access Layer访问层&#xff1f; 二、Access payload 2.1 Opcode 三、Access layer behavior 3.1 Access layer发送消息的流程 3.2 Access layer接收消息的流程 3.3 Unacknowledged and acknowledged messages 3.3.1 Unacknowledged message …...

Netlink 通信机制

文章目录 前言一、Netlink 介绍二、示例代码参考资料 前言 一、Netlink 介绍 Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。 在Linux 内核中&#xff0c;使用netlink 进行应用与内核通信的应用有…...

2024.1.8每日一题

LeetCode 回旋镖的数量 447. 回旋镖的数量 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定平面上 n 对 互不相同 的点 points &#xff0c;其中 points[i] [xi, yi] 。回旋镖 是由点 (i, j, k) 表示的元组 &#xff0c;其中 i 和 j 之间的距离和 i 和 k 之间的欧式…...

看了致远OA的表单设计后的思考

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/n…...

mmdetection训练自己的数据集

mmdetection训练自己的数据集 这里写目录标题 mmdetection训练自己的数据集一&#xff1a; 环境搭建二&#xff1a;数据集格式转换(yolo转coco格式)yolo数据集格式coco数据集格式yolo转coco数据集格式yolo转coco数据集格式的代码 三&#xff1a; 训练dataset数据文件配置config…...

MySQL取出N列里最大or最小的一个数据

如题&#xff0c;现在有3列&#xff0c;都是数字类型&#xff0c;要取出这3列里最大或最小的的一个数字 -- N列取最小 SELECT LEAST(temperature_a,temperature_b,temperature_c) min FROM infrared_heat-- N列取最大 SELECT GREATEST(temperature_a,temperature_b,temperat…...

编写.NET的Dockerfile文件构建镜像

创建一个WebApi项目&#xff0c;并且创建一个Dockerfile空文件&#xff0c;添加以下代码&#xff0c;7.0代表的你项目使用的SDK的版本&#xff0c;构建的时候也需要选择好指定的镜像tag FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443F…...

【C语言】浙大版C语言程序设计(第三版) 练习7-4 找出不是两个数组共有的元素

前言 最近在学习浙大版的《C语言程序设计》&#xff08;第三版&#xff09;教材&#xff0c;同步在PTA平台上做对应的练习题。这道练习题花了比较长的时间&#xff0c;于是就写篇博文记录一下我的算法和代码。 2024.01.03 题目 练习7-4 找出不是两个数组共有的元素 作者 张彤…...

7.27 SpringBoot项目实战 之 整合Swagger

文章目录 前言一、Maven依赖二、编写Swagger配置类三、编写接口配置3.1 控制器Controller 配置描述3.2 接口API 配置描述3.3 参数配置描述3.4 忽略API四、全局参数配置五、启用增强功能六、调试前言 在我们实现了那么多API以后,进入前后端联调阶段,需要给前端同学提供接口文…...

创建第一个SpringMVC项目,入手必看!

文章目录 创建第一个SpringMVC项目&#xff0c;入手必看&#xff01;1、新建一个maven空项目&#xff0c;在pom.xml中设置打包为war之前&#xff0c;右击项目添加web框架2、如果点击右键没有添加框架或者右击进去后没有web框架&#xff0c;点击左上角file然后进入项目结构在模块…...

go 切片长度与容量的区别

切片的声明 切片可以看成是数组的引用&#xff08;实际上切片的底层数据结构确实是数组&#xff09;。在 Go 中&#xff0c;每个数组的大小是固定的&#xff0c;不能随意改变大小&#xff0c;切片可以为数组提供动态增长和缩小的需求&#xff0c;但其本身并不存储任何数据。 …...

回归和分类区别

回归任务&#xff08;Regression&#xff09;&#xff1a; 特点&#xff1a; 输出是连续值&#xff0c;通常是实数。任务目标是预测或估计一个数值。典型应用包括房价预测、销售额预测、温度预测等。 目标&#xff1a; 最小化预测值与真实值之间的差异&#xff0c;通常使用…...

docker nginx滚动日志配置

将所有日志打印到控制台 nginx.conf user nginx; worker_processes auto; # 日志打印控制台 error_log /dev/stdout; #error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;events {worker_connections 1024; }http {include /etc/nginx/m…...

大数据分析案例-基于LinearRegression回归算法构建房屋价格预测模型

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…...

React-hook-form-mui(一):基本使用

前言 在项目开发中&#xff0c;我们选择了ReactMUI作为技术栈。在使用MUI构建form表单时&#xff0c;我们发现并没有与antd类似的表单验证功能&#xff0c;于是我们选择了MUI推荐使用的react-hook-form-mui库去进行验证。但是发现网上关于这个库的使用方法和demo比较少且比较简…...

python总结-生成器与迭代器

生成器与迭代器 生成器生成器定义为什么要有生成器创建生成器的方式一(生成器表达式) 创建生成器的方式二(生成器函数)生成器函数的工作原理总结 迭代器概念可迭代对象和迭代器区别for循环的本质创建一个迭代器 动态添加属性和方法运行过程中给对象、类添加属性和方法types.Met…...

呼和浩特市城乡建设网站/关键词挖掘工具免费

一 HBase的基础架构 1、HMaster 功能&#xff1a; 监控RegionServer处理RegionServer故障转移处理元数据的变更处理region的分配或移除在空闲时间进行数据的负载均衡通过Zookeeper发布自己的位置给客户端 2、RegionServer 功能&#xff1a; 负责存储HBase的实际数据 处理分…...

搞黄色网站做牢/前端seo主要优化哪些

这里将自己学习Go及其区块链的一些笔记、积累分享一下&#xff0c;如果涉及到了文章、文字侵权&#xff0c;请联系我删除或调整。 声明了一个变量&#xff0c;就会在计算机内存中为其分配一个位置&#xff0c;以便存储、修改和访问该变量的值&#xff0c;这个位置就是该变量的地…...

蒙阴蜜桃/seo营销技巧培训班

前言 本文来介绍RocketMQ生产者发送消息默认使用的DefaultMQProducer类。 生产者 向消息队列里写入消息&#xff0c;不 同的业务场景需要生产者采用不同的写入策略 。 比如同步发送、异步发送、 延迟发送、 发送事务消息等。 正文 我们结合代码来了解一下&#xff0c; 代码…...

南充网站建设价格/搜索引擎营销sem

select * from (select * from NA_USEPERCENTPERDAY t order by t.id) where rownum<8;...

衡阳做淘宝网站/百度搜索页

我最近一直在尝试使用BeautifulSoup4&#xff0c;并且已经在各种不同的网站上取得了相当不错的成功 . 虽然我在试图刮掉amazon.com时偶然发现了一个问题 .使用下面的代码&#xff0c;当打印汤我可以看到div&#xff0c;但当我搜索div本身时&#xff0c;BS4带回null . 我认为问题…...

网站建设 品牌塑造计划/国际新闻 军事

MinGW&#xff1a;是指只用自由软件来生成纯粹的Win32可执行文件的编译环境&#xff0c;它是Minimalist GNU on Windows的略称。 MinGW&#xff0c;即 Minimalist GNU For Windows。它是一些头文件和端口库的集合&#xff0c;该集合允许人们在没有第三方动态链接库的情况下使用…...