多线程——CompletableFuture

328 阅读6分钟

CompletableFuture

api

image.png

image.png

image.png

原理

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

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

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

image-20220716171353483.png

各个类含有的私有属性:

Completion:

Completion next;(链表)

UniCompletion<T,V>:

Executor executor;                 // executor to use (null if none)
CompletableFuture<V> dep;          // the dependent to complete
CompletableFuture<T> src; 

BiCompletion<T,U,V>:

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

部分源码分析

join逻辑

public T join() {
    Object r;
    return reportJoin((r = result) == null ? waitingGet(false) : r);
}

任务执行完了(result != null),直接返回,否则调用waitingGet(有阻塞当前线程的逻辑)

阻塞调用链

new Signaller对象 -> 放入当前CompletableFuture的stack的头节点 -> 阻塞当前线程

阻塞【LockSupport.park(在Signaller的block方法中调用)】

唤醒【LockSupport.unpark(在Signaller的tryFire方法中调用)】

示例

System.out.println(new Date());
CompletableFuture<String> one = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(15000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("第一个xx  " + new Date() +"  " + Thread.currentThread().getName());
    return "第一个异步任务";
});
CompletableFuture<String> two = one.thenApply((res) -> {
    try {
        Thread.sleep(20000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("第2个xx  " + new Date() +"  " + Thread.currentThread().getName());
    return "第2个异步任务";
});
CompletableFuture<String> three = one.thenApply((res) -> {
    try {
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("第3个xx  " + new Date() +"  " + Thread.currentThread().getName());
    return "第3个异步任务";
});
System.out.println("   one.join:" + one.join() + "  " + new Date());
System.out.println("   two.join:" + two.join() + "  " + new Date());
System.out.println("   three.join:" + three.join() + "  " + new Date());
System.out.println("end");

执行结果

Mon Jul 18 21:15:55 CST 2022
第一个xx  Mon Jul 18 21:16:10 CST 2022  pool-1-thread-1【one 21:16:10执行完 pool-1-thread-1线程】
第2个xx  Mon Jul 18 21:16:30 CST 2022  main【two 21:16:30执行完 main线程】
   one.join:第一个异步任务  Mon Jul 18 21:16:30 CST 2022【主线程的join21:16:30返回结果】
   two.join:第2个异步任务  Mon Jul 18 21:16:30 CST 2022【主线程的join21:16:30返回结果】
第3个xx  Mon Jul 18 21:16:40 CST 2022  pool-1-thread-1【three 21:16:40执行完 pool-1-thread-1线程】
   three.join:第3个异步任务  Mon Jul 18 21:16:40 CST 2022【主线程的join21:16:40返回结果】
end

可以看到one.join返回结果的时间和one任务执行完的时间是不一致的,具体原因如下

分析

执行完

CompletableFuture<String> one = CompletableFuture.supplyAsync(fn1);
CompletableFuture<String> two = one.thenApply(fn2);
CompletableFuture<String> three = one.thenApply(fn3);

但还没执行到join方法时,此时的one里的属性如下:

image-20220718020352744.png

主线程one线程
任务编排,此时one的stack依次有three、two
执行one
CompletableFuture d = one,此时的d、one的stack只有three、two【AsyncSupply的run方法】
执行ing……
one.join
阻塞(WAIT)…(此时的one的stack有signaller、three、two)
one执行完成
调用d.postComplete,依次弹栈(此时的d、one的stack有signaller、three、two,此时会唤醒主线程)
RUNNING…
继续往下执行,执行postComplete,弹出this的stack【这里是没锁的,不会存在执行CompletableFuture<?> f = this时,两个线程的this都是一样的,导致重复消费吗】继续执行postComplete
此时主线程消费stack里的一个任务one线程消费stack的一个任务
继续消费…继续消费…
stack为空,返回结果给主线程stack为空,结束

可以看到join是等到清空stack后才返回结果给主线程的,所以join的时间和任务执行时间不一致

备注:不会导致重复消费,多线程执行postComplete,在【this】这里只会让一个线程执行,其他线程重新循环

final void postComplete() {
    /*
     * On each step, variable f holds current dependents to pop
     * and run.  It is extended along only one path at a time,
     * pushing others to avoid unbounded recursion.
     */
    CompletableFuture<?> f = this; Completion h;
    while ((h = f.stack) != null ||
           (f != this && (h = (f = this).stack) != null)) {
        CompletableFuture<?> d; Completion t;
        if (f.casStack(h, t = h.next)) {【thisif (t != null) {
                if (f != this) {
                    pushStack(h);
                    continue;
                }
                h.next = null;    // detach
            }
            f = (d = h.tryFire(NESTED)) == null ? this : d;
        }
    }
}

当前任务是由哪个线程执行的

1、有多线程在同时执行postComplete方法

例如上面的例子,任务的执行线程是这几个线程的其中一个

2、没有多线程在同时执行postComplete方法

1)、同步方法(即不带Async后缀的方法,如thenApply、thenAccept),有两种情况

  • 如果注册时被依赖的操作已经执行完成,则由当前线程执行
  • 如果注册时被依赖的操纵还没执行完,则由回调线程执行
CompletableFuture<String> one = CompletableFuture.supplyAsync(fn1, threadpool);
CompletableFuture<String> two = one.thenApply(fn2);
CompletableFuture<String> three = one.thenApply(fn3);

此时one、two、three任务是由threadpool线程执行的,且执行顺序是one -> three -> two

2)、异步方法(即带Async后缀的方法,如thenApplyAsync、thenAcceptAsync)

此时one、two、three由threadpool线程池分配线程执行

CompletableFuture<String> one = CompletableFuture.supplyAsync(fn1, threadpool);
CompletableFuture<String> two = one.thenApplyAsync(fn2, threadpool);
CompletableFuture<String> three = one.thenApplyAsync(fn3, threadpool);

结果

Mon Jul 18 21:41:04 CST 2022
第一个xx  Mon Jul 18 21:41:19 CST 2022  pool-1-thread-1
   one.join:第一个异步任务  Mon Jul 18 21:41:19 CST 2022
第2个xx  Mon Jul 18 21:41:39 CST 2022  pool-1-thread-2
   two.join:第2个异步任务  Mon Jul 18 21:41:39 CST 2022
第3个xx  Mon Jul 18 21:41:49 CST 2022  pool-1-thread-3
   three.join:第3个异步任务  Mon Jul 18 21:41:49 CST 2022
end

stack的存取逻辑

后续补充

参考链接

CompletableFuture原理与实践-外卖商家端API的异步化【美团技术团队】

www.whywhy.vip/archives/13…

juejin.cn/post/697055…

ForkJoin框架之CompletableFuture【详细的源码解读】