前言
有天问一朋友为啥喜欢用把逻辑写在前端,他回答:“Promise 用的很爽”。其它后端也有相对应的CompletableFuture。
CompletableFuture 原理
先说Future, 它用来描述一个异步计算的结果。isDone方法可以用来检查计算是否完成,get方法可以用来获取结果,直到完成前一直阻塞当前线程,cancel方法可以取消任务。而对于结果的获取,只能通过阻塞(get())或者轮询的方式[while(!isDone)]. 阻塞的方式违背了异步编程的理念,轮询的方式耗费无谓的CPU资源(CPU空转)。
核心思想
以下是CompletableFuture类的静态方法,也是入口代码:
CompletableFuture.runAsync(()->{});
CompletableFuture.supplyAsync(()->{ return "";});
//其核心原来都是通过代理runnable
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();
}
}
依赖的构建
CompletableFuture对象中维护着一个stack,并通过以下方式,向栈中压入任务:
future.whenComplete();
future.thenApply();
future.thenAccept();
future.thenRun();
private CompletableFuture<T> uniWhenCompleteStage(Executor e, BiConsumer<? super T, ? super Throwable> f) {
if (f == null)
throw new NullPointerException();
CompletableFuture<T> d = new CompletableFuture<T>();
if (e != null || !d.uniWhenComplete(this, f, null)) {
//将新的Future包装成Complete
UniWhenComplete<T> c = new UniWhenComplete<T>(e, d, this, f);
//把新的CompletableFuture压入stack中
push(c);
// 先调一下钩子方法,检查一下任务是否结束
c.tryFire(SYNC);
}
return d;
}
每个CompletableFuture持有一个Completion栈stack, 每个Completion持有一个CompletableFuture -> dep, 如此递归循环下去,是层次很深的树形结构,所以想办法将其变成链表结构。
依赖的传递
如果当前任选执行完,全根据stack触发依赖任务
final void postComplete() {
CompletableFuture<?> f = this;
Completion h;
//取出stack的Completion放到h中状态消费
while ((h = f.stack) != null || (f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d;
Completion t;
//cas方式推进stack
if (f.casStack(h, t = h.next)) {
//排队h已经被其它线程执行过
if (t != null) {
//Completion依赖的CompletableFuture发生变更时
if (f != this) {
//放回队列
pushStack(h);
continue;
}
h.next = null;
}
//真正的调用执行Completion,当NESTED模式把引Completion转换成到上一级的CompletableFuture,Future依赖变更
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
首先取出头结点,下图中灰色Completion结点,它会返回一个CompletableFuture, 同样也拥有一个stack,策略是遍历这个CompletableFuture的stack的每个结点,依次压入到当前CompletableFuture的stack中,关系如下箭头所示,灰色结点指的是处理过的结点。
第一个Completion结点返回的CompletableFuture, 将拥有的stack里面的所有结点都压入了当前CompletableFuture的stack里面
多线程去stack取任务,执行完子任务,如果发现子任务也有依赖任务则把其子任务压入主任务的stack
线程的异步执行
//claim方法只有在tryFire的mode<=0的时候才可以被调用。
final boolean claim() {
Executor e = executor;
//保证只被执行一次
if (compareAndSetForkJoinTaskTag((short)0, (short)1)) {
//claim方法返回true,表示调用claim方法的线程可以执行fn逻辑处理
if (e == null)
return true;
executor = null;
//异步执行
e.execute(this);
}
return false;
}
//postFire是在CompletableFutrue.result完成赋值之后执行的
final CompletableFuture<T> postFire(CompletableFuture<?> a, int mode) {
// 被依赖的任务存在,且stack不为空,先处理它
if (a != null && a.stack != null) {
// 如果是嵌套模式(mode = -1), 或者任务的结果为空,清理下已失效的任务
if (mode < 0 || a.result == null)
a.cleanStack();
else
a.postComplete();
}
// 再处理当前任务
if (result != null && stack != null) {
// 嵌套模式,直接返回自身(树 -> 链表,避免过深的递归调用)
if (mode < 0)
return this;
else
postComplete();
}
return null;
}
claim
- tryFire在什么时候mode>0?那就是线程池通过run进而调用tryFire的时候。
- 而mode<=0的时候原线程在调用Completion.postComplete方法做处理,异步处理已经可以执行的fn会提高效率。
postFire- 在执完主任务之后,会帮忙完成父类的任务
任务策略
CompletableFuture还操作了强大 “或” 和 “且” 的功能,函数如下
XXXEitherXXX()
XXXBothXXX()
- BiCompletion是一个有2依赖源的Completion
- CoCompletion是一个代理Completion
任务树
根据上面提供的 “或” 和 “且” 的功能,CompletableFuture提供了
allOf()
anyOf()
allOf和anyOf都用到了数组构建成树的策略。假设有一个任务Z(虚拟的,什么都不做),依赖一组任务[A, B, C, D, E, F, G, H]
如果是奇数:h任务没有 , 为 z22 -> (G, G). z22的左右子节点都为G
- 对于allOf, Z只要保证Z1和Z2都完成了就行,Z1和Z2分别保证Z11,Z12 和 Z21,Z22都完成了就像,而Z11,Z12,Z21,Z22则分别保证了A-H任务都完成。
- 对应anyOf, Z 只要保证Z1和Z2有一个完成了就像,Z1和Z2联合保证了Z11,Z12,Z21,Z22这4个任务只要有一个完成了就行,同理,Z11,Z12,Z21,Z22则联合保证了A-H中有一个任务完成了就行。