CompletableFuture原理浅析(JDK1.8.0_241)

本文源码版本1.8.0_241,更高版本源码会有所不同。

常量

CompletableFuture类里只有2个常量

volatile Object result;// Either the result or boxed AltResult

volatile Completion stack;// Top of Treiber stack of dependent actions

result是用来保存任务执行的结果,stack是用来保存阻塞等待结果的线程封装对象,

abstract static class Completion extends ForkJoinTask implements Runnable, AsynchronousCompletionTask {

    volatile Completion next;// Treiber stack link

    /**

    * Performs completion action if triggered, returning a

    * dependent that may need propagation, if one exists.

    *

    * @param mode SYNC, ASYNC, or NESTED

    */

    abstract CompletableFuture tryFire(int mode);

    /** Returns true if possibly still triggerable. Used by cleanStack. */

    abstract boolean isLive();

    public final void run()                { tryFire(ASYNC); }

    public final boolean exec()            { tryFire(ASYNC); return true; }

    public final Void getRawResult()      {return null; }

    public final void setRawResult(Void v) {}

}

从名字可以看出,这些等待结果的线程封装对象是以栈结构保存的,后文会说到。2个变量都是以volatile修饰的,保证可见性。


带返回结果的执行方法

CompletableFuture中提供了2种执行方法,一种没有返回结果,另一种有返回结果,先看有返回结果的

public static <U>  CompletableFuture<U> supplyAsync(Supplier<U> supplier) {

        return asyncSupplyStage(asyncPool, supplier);

}

以supplyAsync为例,进到asyncSupplyStage方法

asyncSupplyStage

创建了一个CompletableFuture对象并作为参数传给了AsyncSupply对象,很明显,AsyncSupply对象是一个封装的任务类,

AsyncSupply

里面有2个变量,一个CompletableFuture用来保存运行结果,一个Supplier对象,函数式接口,保存执行的方法,内部有个run方法用来执行方法,核心就是d.completeValue(f.get()); 进去completeValue看看

completeValue

这里使用到了大名鼎鼎的UNSAFE对象进行cas操作,cas方法有4个参数,分别是对象,变量在内存上在对象内部的相对偏移地址,期望值,更新值,这里先说下这个RESULT

他是在CompletableFuture初始化时,通过反射获取到result变量在CompletableFuture对象里的内存地址偏移量,然后保存了起来;所以上面cas操作的意思是如果RESULT位置处的result变量为null,那么就更新值为后面的三元判断表达式得出的值,而result变量默认值是为null的,所以如果运行的方法结果为null,那么返回NIL对象,NIL是什么

NIL

可以看到是一个封装了null的对象,所以如果执行的方法结果是null,那么返回一个封装了null值的对象,否则返回执行的结果。

回到一开始的run方法,可以看到在方法执行结束后执行了一个d.postComplete()方法

postComplete

如果该方法执行比较耗时,而在其他地方有线程阻塞等待方法执行的结果,那么在这里会将那些阻塞等待的方法唤醒,可以看到while循环里的表达式,(h = f.stack) !=null,如果有线程阻塞等待的话,这个h是不为null的,那么会进入到循环里,在方法的最后执行tryFire方法,

tryFire

可以看到这个方法是将阻塞等待的线程唤醒,如果此时有多个线程阻塞等待的话,那么通过if条件里的f.casStack(h, t = h.next)这个cas操作,会依次将所有线程唤醒。

另外,该方法还有一个同名方法,有2个参数,支持传入自定义的线程池

另外,值得注意的一点是,如果不传入自定义线程池的话,使用的是底层自带的ForkJoinPool池,该池的线程均为守护线程,所以使用时需要注意,可能出现线程池里的线程还没执行完,外面的主线程已经执行结束了,可以调用ForkJoinPool的awaitQuiescence方法等待线程池里的线程执行结束。


不带返回结果的执行方法

以runAsync方法为例

runAsync

同样的套路,进入asyncRunStage方法

asyncRunStage

一样的封装了个AsyncRun对象

AsyncRun

可以看到大体的结构和之前是一样的,唯一的不同在于,在方法执行结束后调用了d.completeNull()这个方法,

completeNull

这个cas操作判断result变量是否为null,为null就更新为之前说的那个封装了null值的对象,而result变量默认是为null的,所以直接返回一个封装了null值的对象,因为run方法是没有返回值的,所以直接就这样返回了。其他的逻辑和之前说的一样。


get方法

接下来看看get方法

get

如果调用get方法时,方法还未执行完成,那么此时result是为null的,调用waitingGet方法

waitingGet

可以看到会进入一个while循环里,前2个if是自旋一段非常短的时间,在更高版本的源码中,这2个if被删除了,此处有个有意思的东西,可以看到有一个SPINS常量,这个常量是什么呢,

SPINS

他调用了系统函数判断机器是否是多核CPU,如果是则赋值256,否则为0,这个变量在之前的版本里是写在方法里的,但是会有性能问题,所以后来抽出来了,有兴趣的可以看看https://bugs.openjdk.java.net/projects/JDK/issues/,我们接着往下看,当前2个if执行完后,执行到第三个if,会构建一个Signaller对象

Signaller

可以看到里面有一个thread变量保存了阻塞等待的线程,前面说的tryFire方法唤醒线程也在这里,接着走到第四个if,执行tryPushStack方法

tryPushStack

这里就是把所有阻塞等待的线程用栈保存起来,怎么做的呢,首先获取CompletableFuture中的stack对象,是个null值,然后将封装阻塞等待线程的Signaller对象中的next变量指向这个stack,

lazySetNext

可以看到这里直接进行了赋值,没有走cas操作,因为这个Signaller对象,不同的线程是不一样的,相互独立的,所以没有必要进行cas操作,但是下一步UNSAFE.compareAndSwapObject(this,STACK, h, c),将CompletableFuture中的stack变量替换为封装自身线程的Signaller对象时,会有竞争存在,所以需要走cas保证线程安全性,执行失败的线程会重新走一遍while循环走到这里,直到设置成功。下面一个if是判断线程中断的,我们跳过,最后一个if会执行managedBlock方法

managedBlock

里面核心是 !blocker.isReleasable() && !blocker.block()这个条件,isReleasable方法恒为false,我们看block方法,

block

可以看到里面将线程挂起了。当方法执行完成后会将他们唤醒,回到get方法里,当拿到结果后执行reportGet方法

reportGet

前面说过,如果方法执行结果为null或者没有返回结果时,会返回一个封装了null值的AltResult对象,这里判断对象是否是AltResult对象,如果是,则判断封装的结果是否为null,是则返回null,不是则抛出相应的异常,如果都不是返回正常执行的结果。

所以,如果有多个线程阻塞等待的话,所有线程会先排个队,用stack数据结构保存起来,然后各自将自己挂起,等方法运行结束后会将这些阻塞线程全部唤醒。

以上即为CompletableFuture方法运行和获取值的原理,还有很多其他的方法,但是底层方法基本都是上面说的那几个。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容