概述
CompletionStage直译是完成阶段,如果你发现你有这么一个需求:一个大的任务可以拆分成多个子任务,并且子任务之间有明显的先后顺序或者一个子任务依赖另一个子任务完成的结果时,那么CompletionStage是一个不错的选择,有点像聚合任务的特点,但Completion可以实现比聚会任务复杂得多的任务交互,CompletionStage就是实现了将一个大任务分成若个子任务,这些子任务基于一定的并行、串行组合形成任务的不同阶段,CompletionStage接口实现了对这些子任务之间的关系定义,接口方法较多如下:
CompletionStage接口的方法虽然方法很多但不难发现有规律可循,因为CompletionState定义的方法名以及方法参数大量使用了jdk基本的函数式接口,简单说明几个基本的函数式接口如下:
- Supplier:产出型函数,该函数式接口将返回一个结果,内部定义了一个 T get()方法;
- Consumer:消耗性函数,接受并消费一个参数,内部定义了一个accept(T t)方法;
- Function:即消耗又产出型函数,接受并消费一个参数同时返回一个结果,内部定义了一个R apply(T t)方法;
- BiConsumer:消费性函数,接受并消费两个参数,内部定义了一个void accept(T t, U u)方法;
- BiFunction:即消耗又产出型函数,接受并消费两个参数并返回一个结果,内部定义了一个R apply(T t, U u)方法;
观察CompletionStage接口的方法发现基本都是由then、apply、async、accept、run、combine、both、either、after、compose、when、handle等关键词组合而成,这些关键字可以理解如下:
- then:表示阶段先后顺序,即一个阶段等待另一个阶段完成
- apply:和上面Function一样,表示消费一个参数并提供一个结果
- async:异步标志,即阶段任务的执行相对于当前线程是同步还是异步
- accept:和上面Consumer的accept方法一样,表示消费一个参数
- run:既不消费也不铲除,同Runnable接口含义
- combine:合并两个阶段结果并返回新的阶段
- both:表示二者条件都成立再做其它事
- either:表示二者之一条件成立再做其它事,对应both
- after:表先后顺序,一个任务发生在另一个任务之后,和then相似
- compose:表示根据已有结果生成新的结果,同上面的Function,细看compose的参数和thenApply有区别,具体区别再下面陈述
- when:等同于whenComplete,当前阶段正常完成或异常完成时执行BiConsumer动作
- handle:当前阶段正常完成或异常完成后触发一个BiFunction动作
此外观察所有接口参数不难发现参数仅包含以下三种:
- 函数式接口:Runnable、Consumer、Function、BiConsumer、BiFunction
- 另一个CompletionStage
- Executor
因此该接口提供的所有方法可以总结成一句话:当前阶段完成(分为正常完成或异常完成)后,根据当前阶段的返回的结果(分有结果或无结果)去执行(分同步或异步,其中异步可以基于自定义的执行器)入参中的函数式接口并返回一个新的CompletionStage
CompletionStage接口方法说明bai
/*
*同步执行:这里的同步执行分两种可能
* 1.调用方法时当前阶段还没有完成,则同步执行表示由当前阶段任务对应线程同步执行
* 2.调用方法时当前阶段已经完成,则同步执行表示由调用方法线程执行
*异步执行:这里异步执行表示将任务提交到线程池由线程池分配线程执行
*执行器:调用方法时可以使用指定的执行器执行任务
*/
//当前阶段正常完成后其结果作为fn(Function)的入参同步执行fn并返回一个新的CompletionStage
public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
//同上,区别在于异步执行fn
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn);
//同上,区别在于使用指定执行器异步执行
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn,Executor executor);
//当前阶段正常完成后其结果作为action(Consumer)的入参同步执行action并返回一个新的CompletionStage
//与thenApply相比,返回的新CompletionStage泛型参数为Void
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
//同上,区别在于异步执行fn
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
//同上,区别在于使用指定执行器异步执行
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor);
//当前阶段正常完成后同步执行指定的action,不需要当前阶段执行结果,任务也不返回结果
public CompletionStage<Void> thenRun(Runnable action);
//同上,区别在于异步执行fn
public CompletionStage<Void> thenRunAsync(Runnable action);
//同上,区别在于使用指定执行器异步执行
public CompletionStage<Void> thenRunAsync(Runnable action, Executor executor);
//当前阶段和参数other阶段同时正常完成后其二者的结果作为fn的入参同步执行fn并返回一个新的CompletionStage
public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
//同上,区别在于异步执行fn
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
//同上,只是在指定的执行器中异步执行
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
//当前阶段和入参中的other阶段均正常完成后,二者的结果作为action的入参同步执行action并返回新的CompletionStage
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
//同上,只是异步执行action
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action);
//同上只是在执行的执行器中异步执行action
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action,Executor executor);
//当前阶段和入参中的other阶段均正常完成后同步执行action
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other, Runnable action);
//同上,异步执行
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action);
//通常,再指定的执行器中异步执行
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor);
//当前阶段或入参中的other阶段二者只要有一个阶段正常完成则其结果作为fn的入参同步执行fn并返回一个新的阶段
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
//同上,异步执行
public <U> CompletionStage<U> applyToEitherAsync (CompletionStage<? extends T> other, Function<? super T, U> fn);
//同上,再指定的执行中异步执行
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn,
Executor executor);
//当前阶段或入参中的other阶段二者之一正常完成时其结果将作为action入参同步执行并返回一个不带结果的新的阶段
public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
//同上,异步执行
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
//同上,再指定的执行中异步执行
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action,
Executor executor);
//当前阶段或入参中的other阶段二者之一正常完成时执action动作
public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action);
//同上,异步执行
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
//同上,再指定的执行中异步执行
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);
//当前阶段正常完成后其结果作为fn的入参同步执行fn并返回一个新的阶段,这里返回的新的阶段是由fn返回的,
//注意区分thenCompose和thenApply,thenApply中的fn直接的返回的是阶段的执行结果,而thenCompose则直接返回的是一个阶段对象
//其差别和Optional的flatMap和map的差别类似
public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
//同上,异步执行
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn);
//同上,再执行器中异步执行
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,Executor executor);
//当前阶段发生异常时其异常结果作为入参同步执行fn并返回新的阶段
public CompletionStage<T> exceptionally(Function<Throwable, ? extends T> fn);
//当前阶段正常或异常完成时,其结果(正常结果或异常对象)作为入参同步执行action并返回新的阶段
public CompletionStage<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);
//同上,异步执行
public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action);
//同上,在执行器中异步执行
public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action,Executor executor);
//当前阶段正常完成或异常完成时其结果作为fn入参同步执行fn并返回一个指定类型U的新的姐u但
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
//同上,异步执行
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
//同上,在执行器中异步执行
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
//将当前CompletionStage转换为对等的CompletableFuture,如果当前CompletionStage已经是一个CompletableFuture,则返回本身
//否则该操作等同于thenApply(e->e);
public CompletableFuture<T> toCompletableFuture();
CompletableFuture概述
CompletableFuture类继承关系如下:其实现了两个接口:CompletionStage、Future
CompletableFuture具备两个属性:
- result:即阶段执行结果,阶段任务执行完成后其结果将保存到该属性上
- stack:一个Completion类型的实例,Completion本身具备Completion类型的next成员变量,因此其构成了栈结构,实际上Completion本身继承自ForkJoinTask,因此保存了异步任务相关信息,该属性表示依赖当前阶段(CompletionStage)的任务链,Completion作为一个抽象类,其类层次图如下:
从上图可以看出所有的所有关于Complection的子类情况,除Signaller和CoCompletion以外,所有的其它实现类均继承自UniComplection,而UniComplection中类成员结构入下,因此可得出Complection不仅维护任务信息,还维护了任务的依赖源和任务对应的阶段信息,其阶段信息又维护依赖当前任务阶段的其它阶段信息,如此形成了一个阶段依赖链
//UniCompletion中并没有定义与任务有关的信息,具体任务信息有子类实现
abstract static class UniCompletion<T,V> extends Completion {
//依赖任务的执行环境,如果executor不为null则说明是异步执行,否则为同步执行或内嵌执行
Executor executor; // executor to use (null if none)
//当前任务对应的阶段对象,任务运行结果将保存到dep.result中,同时dep.stack又是一个依赖栈
CompletableFuture<V> dep; // the dependent to complete
//当前任务依赖的阶段,只有src阶段完成后该任务才能被触发执行
CompletableFuture<T> src;
···
···
}
因此CompetableFutre.stack信息可以描述为下图所示结构:当一个CompletableFuture完成时,要依次触发依赖该阶段的Completion栈、以及Completion本身dep属性的stack栈
当阶段完成时需要触发对应的依赖任务(即上图中的stack栈上的任务),根据stack栈任务被触发的方式分为以下3种
- NESTED:由执行阶段任务对应线程触发,即对应下面案例种的路径1
- SYNC:由创建依赖任务的线程触发,即对应下面案例种的路径2
- ASYNC:当异步执行任务时,异步任务并不时对应src阶段完成时调用方法执行,而是将任务提交到线程池执行,其dep的依赖栈将由任务执行完成后触发
CompletableFuture源码分析
单阶段依赖
分析以下测试代码执行流程,其结果是最后提交的任务处于stack栈顶,先提交的任务处于栈底,因此应首先执行5秒休眠任务后再执行异步打印任务,因此如果想要异步任务立即被执行,应该最后提交异步任务;鉴于CompletableFuture默认采用ForkJoinPool作为执行器,因此如果提交的多个异步任务均为阻塞任务(如IO访问),则同样会导致任务执行阻塞,此时可以使用ForkJoinPool的ManagedBlocker创建补偿任务防止IO阻塞导致执行器吞吐量下降
//返回时间和当前线程信息的字符串
static String getTimeAndThreadInfo() {
return DateTimeFormatter.ISO_TIME.format(LocalTime.now()) + " " + Thread.currentThread().getName();
}
//休眠指定毫秒,忽略IterruptedException
static void sleep(int ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
}
}
public static void main(String[] args) {
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> getTimeAndThreadInfo() + "==>1");
cf1.thenAcceptAsync(f1-> System.out.println("cf1完成后异步打印cf1结果:"+f1));
cf1.thenAccept(f1->sleep(5000));
sleep(1000000);
}
分析路径1
- 路径1说明如下:即main方法中执行到sleep时,cf1阶段还没有完成,因此依赖cf1阶段的任务将由异步任务完成时触发,这也是比较正常的
supplyAsync:CompletableFuture.supplyAsync静态方法创建一个CompletableFuture并异步执行生产型任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
asyncSupplyStage:创建一个新的CompletableFuture阶段,并将生成性任务封装成ForkJoinTask任务提交到线程池中
static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f));
return d;
}
AsyncSupply:作为异步生产任务的封装类,内部维护了任务对应的阶段信息dep,当任务执行完成时会Supplier结果设置到对应阶段的结果中,并尝试触发依赖该阶段的其它阶段任务,注意当任务null或任务发生异常时,其null结果或异常信息将被封装成AltResult对象(如果存在异常信息则AltResult.ex为对应异常,否则说明结果为null),因此可以通过CompletableFuture.result != null来判断该阶段是否已完成。
static final class AsyncSupply<T> extends ForkJoinTask<Void>
implements Runnable, AsynchronousCompletionTask {
CompletableFuture<T> dep; Supplier<T> fn;
AsyncSupply(CompletableFuture<T> dep, Supplier<T> fn) {
this.dep = dep; this.fn = fn;
}
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) {}
public final boolean exec() { run(); return true; }
public void run() {
CompletableFuture<T> d; Supplier<T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null;
if (d.result == null) {
try {
//生产性任务执行完成将结果设置到任务对应阶段结果
d.completeValue(f.get());
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
//尝试触发依赖该阶段的其它阶段任务
d.postComplete();
}
}
}
postComplete:该方法将在尝试触发所有所有依赖当前阶段的其它阶段的任务,并且这种触发是链式传递的,不仅是依赖该阶段的任务会被触发,整个依赖链上的任务会按照入栈顺序依次触发,因此先入栈的任务会在后入栈的同步任务执行完成后才被触发(后边以一个简单例子说明),这里需要区分同步执行和异步执行任务,具体如下:
- 同步任务:当前线程按照栈顺序(后入先出)依次触发依赖阶段的任务
- 异步任务:当前阶段将异步执行该任务,其依赖该异步任务的后续依赖阶段将由异步任务完成后其所在线程触发,和当前阶段线程脱钩
final void postComplete() {
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)) {//触发一个Completion后将该Complection从栈中移除
if (t != null) {//如果触发栈还存在待触发Complection
if (f != this) {//该触发栈非当前触发栈(依赖当前阶段的其它阶段的触发栈),则将其它阶段的触发栈的待触发任务加入到当前触发栈
pushStack(h);
continue;
}
h.next = null; // detach,帮助gc
}
//尝试以内嵌方式触发依赖阶段的其它阶段任务,任务执行的方式由任务本身是否同步或异步方式决定,tryFile由具体的Completion实现
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
tryFire:tryFile依赖具体的Completion实现,这里以UniAccept进行说明:执行uniAccept方法尝试触发Completion任务(当前方法中可能成功也可能不成功,这是因为该方法在多个场景下被调用,如果处于上述调用链上则必然成功),成功后在依次触发依赖当前阶段的其它阶段的触发栈任务
final CompletableFuture<Void> tryFire(int mode) {
CompletableFuture<Void> d; CompletableFuture<T> a;
if ((d = dep) == null ||
//uniAccept方法尝试直接触发依赖任务,如果不成功则不在进行后续触发逻辑
!d.uniAccept(a = src, fn, mode > 0 ? null : this))
return null;
dep = null; src = null; fn = null;
//执行postFile触发当前阶段依赖阶段任务
return d.postFire(a, mode);
}
uniAccept:尝试触发Completion对应任务,Completion保存了依赖源阶段和依赖目标阶段,因此可以判断是否可以触发以及触发后设置执行结果到依赖目标阶段上
final <S> boolean uniAccept(CompletableFuture<S> a,
Consumer<? super S> f, UniAccept<S> c) {
Object r; Throwable x;
//r=a.result为null说明依赖源阶段还未完成,不能执行触发任务
if (a == null || (r = a.result) == null || f == null)
return false;
//当前阶段为空说明还没有触发过,这里的当前阶段指依赖目标阶段,实际上下面通过claim可以保证只触发依次,使用该判可以减少claim判断次数
tryComplete: if (result == null) {
//如果结果为AltResult则说明依赖源结果要么为null要么发生异常,对于前者传递异常,
//非CompletionException将被封装成CompletionException异常并不在触发依赖任务直接设置异常结果并传递传递到整个依赖链
if (r instanceof AltResult) {
if ((x = ((AltResult)r).ex) != null) {
completeThrowable(x, r);
break tryComplete;
}
r = null;
}
try {
//在真正触发任务前通过claim操作防止重复触发,claim操作为基于cas的原子操作能保证原子性
//这里c != null是因为入参c为null意味着当前Completion还没有入栈,因此不可能出现并发情况
//并发情况发生在Completion入栈后再次尝试执行触发任务,如果此时依赖源任务刚好执行完成并弹出Completion并尝试触发则出现并发
if (c != null && !c.claim())
return false;
@SuppressWarnings("unchecked") S s = (S) r;
f.accept(s);
completeNull();
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}
claim:基于cas操作将当前Complection的任务状态标识为1(已触发),判断当前Completion是否存在执行器,如果存在则说明当前Completion属于异步执行,提交到线程池后返回false不在触发该Complection的后继触发任务;否则将一直触发后继任务
final boolean claim() {
Executor e = executor;
if (compareAndSetForkJoinTaskTag((short)0, (short)1)) {
if (e == null)
return true;
executor = null; // disable
e.execute(this);
}
return false;
}
postFire:该方法用于触发完栈中的Completion本身的任务后接着触发该Completion上的依赖阶段任务
final CompletableFuture<T> postFire(CompletableFuture<?> a, int mode) {
//当前阶段存在依赖任务
if (a != null && a.stack != null) {
//内嵌触发模式或依赖源阶段还没有完成则清除已取消任务
if (mode < 0 || a.result == null)
a.cleanStack();
else//非NESTED模式且当前阶段已完成则执行触发依赖阶段任务
a.postComplete();
}
//当前阶段已完成且存在依赖阶段
if (result != null && stack != null) {
if (mode < 0)//如果处于NESTED模式,则返回该阶段以内嵌模式循环遍历触发依赖任务
return this;
else//正常触发模式
postComplete();
}
return null;
}
分析路径2
- 分析路径2说明:相对于路径1,路径2在main方法中执行thenAcceptAsync或thenAccept时异步cf1阶段已经完成,此时流程如下:
//同步模式
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
return uniAcceptStage(null, action);
}
//异步模式,使用ForkJoinPoll的commonPool作为执行器(默认,可以自定义为每个任务创建线程执行)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
return uniAcceptStage(asyncPool, action);
}
//指定执行器异步模式
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor) {
return uniAcceptStage(screenExecutor(executor), action);
}
上述三种模式均调用uniAcceptStage实现,源码如下:
private CompletableFuture<Void> uniAcceptStage(Executor e,Consumer<? super T> f) {
if (f == null) throw new NullPointerException();
//为当前任务创建一个新的阶段对象
CompletableFuture<Void> d = new CompletableFuture<Void>();
//如果非异步执行,则尝试直接触发当前任务,前面关于该方法已经说明,该方法通过判断当前阶段是否已经完成并基于cas操作保证并发情况下触发任务
//这里第三个参数==null说明无需进行cas竞争直接触发任务,因为此时该任务还没有入栈因此不存在并发情况
//异步执行将在入栈后通用方式触发异步执行
if (e != null || !d.uniAccept(this, f, null)) {
//创建Completion任务
UniAccept<T> c = new UniAccept<T>(e, d, this, f);
//将当前Completion任务加入到当前阶段的触发任务栈中
push(c);
//再次尝试触发任务(同步模式),这是因为有可能在前面入栈结束后当前阶段刚好完成,这种情况不进行再次触发的化会导致依赖阶段得不到触发
c.tryFire(SYNC);
}
return d;
}
push:当前阶段未完成则循环入栈
final void push(UniCompletion<?,?> c) {
if (c != null) {
//如果出现tryPushStack失败,则必然时因为由并发线程也在入栈导致,因此需要清除当前c.next属性重新设置
while (result == null && !tryPushStack(c))
lazySetNext(c, null); // clear on failure
}
}
多阶段依赖
当一个阶段依赖于多个阶段时与单阶段依赖有所不同,参见下面测试例子,需求为:A、B、C、D共4个任务并发执行,要求任务D必须等待任务A、B、C执行完成后获取其结果后再执行,测试代码如下:
public class CompletionStageTest {
static String getTimeAndThreadInfo() {
return DateTimeFormatter.ISO_TIME.format(LocalTime.now()) + " " + Thread.currentThread().getName();
}
static void sleep(int ms) {
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
}
}
public static void main(String[] args) {
//预热
IntStream.range(0,3).forEach(i-> new RecursiveAction() {
@Override
protected void compute() {
sleep(100);
}
});
long start = System.currentTimeMillis();
CompletableFuture<String> A = CompletableFuture.supplyAsync(() -> {
sleep(3000);
System.out.println("任务A执行完成,耗时:"+(System.currentTimeMillis() - start));
return getTimeAndThreadInfo() + "==>A";
});
CompletableFuture<String> B = CompletableFuture.supplyAsync(() -> {
sleep(1500);
System.out.println("任务B执行完成,耗时:"+(System.currentTimeMillis() - start));
return getTimeAndThreadInfo() + "==>B";
});
CompletableFuture<String> C = CompletableFuture.supplyAsync(() -> {
sleep(2000);
System.out.println("任务C执行完成,耗时:"+(System.currentTimeMillis() - start));
return getTimeAndThreadInfo() + "==>C";
});
A.thenCombine(B, (a, b) -> new Object[]{a, b}).thenAcceptBoth(C,(ab,c)->{
System.out.println(getTimeAndThreadInfo()+"===>D:打印任务A/B/C的结果:");
System.out.println(ab[0]);
System.out.println(ab[1]);
System.out.println(c);
System.out.println("任务D执行完成,耗时:"+(System.currentTimeMillis() - start));
});
sleep(10000);
}
}
测试结果如下:可以看到任务A/B/C的耗时和休眠时间差不多,不存在阻塞排队情况,任务D的耗时时间为A/B/C种耗时最长的时间,符合预期要求
任务B执行完成,耗时:1519
任务C执行完成,耗时:2012
任务A执行完成,耗时:3015
19:04:59.475 ForkJoinPool.commonPool-worker-1===>D:打印任务A/B/C的结果:
19:04:59.475 ForkJoinPool.commonPool-worker-1==>A
19:04:58.006 ForkJoinPool.commonPool-worker-2==>B
19:04:58.472 ForkJoinPool.commonPool-worker-3==>C
任务D执行完成,耗时:3015
thenCombine相关的三个方法用于依赖的两个阶段均正常完成后二者的结果作为入参执行fn操作,区别在于同步、异步、制定执行中执行,最终都调用biApplyStage方法实现
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn) {
return biApplyStage(null, other, fn);
}
public <U,V> CompletableFuture<V> thenCombineAsync(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn) {
return biApplyStage(asyncPool, other, fn);
}
public <U,V> CompletableFuture<V> thenCombineAsync(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn, Executor executor) {
return biApplyStage(screenExecutor(executor), other, fn);
}
biApplyStage如下,大体逻辑与uniApplyStage相差不大
private <U,V> CompletableFuture<V> biApplyStage(
Executor e, CompletionStage<U> o,
BiFunction<? super T,? super U,? extends V> f) {
CompletableFuture<U> b;
if (f == null || (b = o.toCompletableFuture()) == null)
throw new NullPointerException();
CompletableFuture<V> d = new CompletableFuture<V>();
//同uniApplyStage一样,若非异步模式,尝试直接触发任务执行(仅当当前阶段和依赖阶段已完成时成功)
//否则创建Completion任务入栈,具体看入栈流程和入栈后的触发流程
if (e != null || !d.biApply(this, b, f, null)) {
BiApply<T,U,V> c = new BiApply<T,U,V>(e, d, this, b, f);
bipush(b, c);
c.tryFire(SYNC);
}
return d;
}
先看一下BiApply的结构,其继承自BiCompletion,带Bi开头说明任务依赖2个阶段,前面的例子都讨论的时依赖一个阶段(src)
//BiApply继承自BiCompletion,主要实现了tryFire方法用于双依赖情况下的任务触发逻辑
static final class BiApply<T,U,V> extends BiCompletion<T,U,V> {
BiFunction<? super T,? super U,? extends V> fn;
BiApply(Executor executor, CompletableFuture<V> dep,
CompletableFuture<T> src, CompletableFuture<U> snd,
BiFunction<? super T,? super U,? extends V> fn) {
super(executor, dep, src, snd); this.fn = fn;
}
final CompletableFuture<V> tryFire(int mode) {
CompletableFuture<V> d;
CompletableFuture<T> a;
CompletableFuture<U> b;
if ((d = dep) == null ||
!d.biApply(a = src, b = snd, fn, mode > 0 ? null : this))
return null;
dep = null; src = null; snd = null; fn = null;
return d.postFire(a, b, mode);
}
}
//BiCompletion定义了双依赖概念,其继承自UniCompletion主要增加了一个CompletableFuture类型的snd变量用于保存当前任务的另一个依赖源
abstract static class BiCompletion<T,U,V> extends UniCompletion<T,V> {
CompletableFuture<U> snd; // second source for action
BiCompletion(Executor executor, CompletableFuture<V> dep,
CompletableFuture<T> src, CompletableFuture<U> snd) {
super(executor, dep, src); this.snd = snd;
}
}
biApply:尝试直接触发任务,仅再当前阶段和依赖的另一个阶段均正常完成且非异步执行的情况下被调用
final <R,S> boolean biApply(CompletableFuture<R> a,
CompletableFuture<S> b,
BiFunction<? super R,? super S,? extends T> f,
BiApply<R,S,T> c) {
Object r, s; Throwable x;
//入参基本检查,如果依赖的两个阶段任意一个result == null说明还未完成则返回false等待完成再触发
if (a == null || (r = a.result) == null ||
b == null || (s = b.result) == null || f == null)
return false;
//对异常的处理,同uniApply逻辑,发生异常设置依赖结果为异常AltResult并不触发任务执行,从这里可以看到两个依赖源都不能发生异常
tryComplete: if (result == null) {
if (r instanceof AltResult) {
if ((x = ((AltResult)r).ex) != null) {
completeThrowable(x, r);
break tryComplete;
}
r = null;
}
if (s instanceof AltResult) {
if ((x = ((AltResult)s).ex) != null) {
completeThrowable(x, s);
break tryComplete;
}
s = null;
}
try {
//claim方法原子操作保证并发安全、防止重复触发任务
if (c != null && !c.claim())
return false;
@SuppressWarnings("unchecked") R rr = (R) r;
@SuppressWarnings("unchecked") S ss = (S) s;
//触发任务并设置阶段结果
completeValue(f.apply(rr, ss));
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}
bipush:该方法将尝试向两个依赖源的触发栈种入栈当前Completion任务,但由于只能被二者之一触发,因此当二者之一完成一个时,只需要将Completion入栈到未完成的那个即可,如何二者此时皆已完成,则不在入栈,接下来的tryFire方法将触发该Completion任务,若二者均未完成则二者都需要入栈,此种情况下为了保证只被触发一次,因此需要将其中之一的依赖对象封装成代理对象CoCompletion。
/*
*BiCompletion表示该任务关联两个阶段,因此需要将BiCompletion分别入到两个阶段的完成触发栈中,具体如下:
* 1.如果当前阶段未完成,则尝试将BiCompletion循环入到当前阶段的完成触发栈中,如果已完成则无需在入当前阶段完成触发栈中。
* 2.如果另一阶段未完成,则根据当前阶段是否完成创建BiCompletion的代理对象(未完成创建,已完成不创建)并尝试循环入到另一阶段的完成触发栈中
*该方法执行过后结果如下:
* 1.BiCompletion或其代理对象分别成功入到当前阶段或另一阶段(入栈的是BiCompletion的代理对象)的完成触发栈中
* 2.BiCompletion没有进入当前阶段和另一阶段的完成触发栈中,原因是入栈过程中两个阶段已完成
* 3.BiCompletion入当前栈中但未入另一阶段栈中,这是因为当前阶段未完成但另一阶段已完成
* 4.BiCompletion没有入当前阶段但入了另一阶段的完成触发栈中,这是因为当前阶段已完成但另一阶段未完成
*/
final void bipush(CompletableFuture<?> b, BiCompletion<?,?,?> c) {
if (c != null) {
Object r;
while ((r = result) == null && !tryPushStack(c))
lazySetNext(c, null); // clear on failure
if (b != null && b != this && b.result == null) {
Completion q = (r != null) ? c : new CoCompletion(c);
while (b.result == null && !b.tryPushStack(q))
lazySetNext(q, null); // clear on failure
}
}
}
关于BiApply的tryFire和CoCompletion的tryFire方法说明:二者均用于双依赖下依赖源触发子任务的调用的Completion方法。前者的逻辑直接通过biApply保证原子性;后者仅封装了一下BiApply并简单的判断了以下base是否已由结果,否则调用biApply方法尝试触发,通过base==null判断一定程度上避免了重复执行biApply去做cas检查要高效。
//BiApply.tryFire
final CompletableFuture<V> tryFire(int mode) {
CompletableFuture<V> d;
CompletableFuture<T> a;
CompletableFuture<U> b;
if ((d = dep) == null ||
!d.biApply(a = src, b = snd, fn, mode > 0 ? null : this))
return null;
dep = null; src = null; snd = null; fn = null;
return d.postFire(a, b, mode);
}
//CoCompletion.tryFile
final CompletableFuture<?> tryFire(int mode) {
BiCompletion<?,?,?> c; CompletableFuture<?> d;
if ((c = base) == null || (d = c.tryFire(mode)) == null)
return null;
base = null; // detach
return d;
}
至此CompletionFuture的源码已经分析完成,其它Completion子类大多大同小异,这里可以简单猜想以下acceptEither的逻辑,与acceptBoth的主要区别在于触发任务逻辑上,acceptEither只要任意一个任务完成就触发任务,orApply用于either依赖任务的触发,源码如下:
final <R,S extends R> boolean orApply(CompletableFuture<R> a,
CompletableFuture<S> b,
Function<? super R, ? extends T> f,
OrApply<R,S,T> c) {
Object r; Throwable x;
if (a == null || b == null ||
//2个依赖源都还没有完成则不触发
((r = a.result) == null && (r = b.result) == null) || f == null)
return false;
tryComplete: if (result == null) {
try {
//cas操作保证原子性,任务只被触发一次
if (c != null && !c.claim())
return false;
if (r instanceof AltResult) {
if ((x = ((AltResult)r).ex) != null) {
completeThrowable(x, r);
break tryComplete;
}
r = null;
}
@SuppressWarnings("unchecked") R rr = (R) r;
completeValue(f.apply(rr));
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}
此外either依赖的入栈也有所区别,从逻辑上来说只有两个依赖源都未完成时才需要入栈到两个依赖源的触发栈中,否则直接触发任务;orpush用于either依赖入栈操作,源码如下
final void orpush(CompletableFuture<?> b, BiCompletion<?,?,?> c) {
if (c != null) {
while ((b == null || b.result == null) && result == null) {//两个依赖源都未完成(或其中一个依赖源未null,实际上已做null校验)
if (tryPushStack(c)) {//当前阶段入栈成功
//另一个阶段未完成时才进行入栈,否则不入栈最终由当前阶段触发,从这里可以看到并不能保证由最先完成的阶段触发任务,
//但二者时间上差异很小,入栈后会立即再次尝试触发任务
if (b != null && b != this && b.result == null) {
Completion q = new CoCompletion(c);
while (result == null && b.result == null &&
!b.tryPushStack(q))
lazySetNext(q, null); // clear on failure
}
break;
}
lazySetNext(c, null); // clear on failure
}
}
}
总结
- CompletionStage定义了异步计算任务之间相互依赖的规范,这些异步计算任务并非仅仅是简单的并发或聚合任务,而是在基于单依赖、both双依赖以及either双依赖下可以构建复杂的异步计算任务执行链,整个执行链可以有多个起点(起始任务),多个终点(终点任务)的任务链。
- CompletableFuture实现了CompletionStage,为了实现一个阶段完成后触发另一个阶段的执行,CompletableFuture内部定义了一个触发执行栈stack,触发执行栈的元素是Completion类型,该类型定义了执行动作(继承自ForkJoinTask,具体执行动作由子类实现)、依赖源(单依赖src,双依赖另一个为snd)、执行动作对应的目标阶段、以及当前任务的执行方式(相对于触发线程而言同步或异步执行),而目标阶段又有触发执行栈,因此可以形成复杂的异步计算任务链。
- 为保证任务只被触发一次,内部通过cas原子操作保障。
- Signaller:该Completion主要用于获取阶段结果上,当阶段任务为完成时,获取结果的线程应该被阻塞,CompletableFuture将获取结果的线程封成Signaller入栈到对应阶段的触发任务中后进入阻塞(park)等待,当任务被触发时当前线程被取消阻塞(unpark),为了保证不占用ForkJoinPool线程池的工作线程数量,使用ForkJoinPool.ManagedBlocker为获取结果线程创建补偿线程。
- CoCompletion:该Completion主要用于阶段双依赖情况下,当阶段依赖两个阶段都未完成时,当前阶段对应的Completion需要分别入两个阶段的触发任务栈中,CoCompletion通过装饰着模式封装当前Completion,其目的在于重写tryFire方法,通过简单的base==null判断可在一定层度上钱少直接调用c.tryFire进行判断