【并发重要原则】happens-before应用之FutureTask

FutureTask可以说是happens-before最经典的应用了。
我们主要看看jdk8 FutureTask

引用
1. why outcome object in FutureTask is non-volatile?
2. 聊聊高并发(十八)理解AtomicXXX.lazySet方法

相信大家看过FutureTask源码的朋友都会对一个outcome变量为什么不加volatile记忆深刻。

我们回顾一下问题:

这个outcome变量没有声明volatile,也就是理论上其他线程是无法及时看到outcome的变化。
而作者特意加上注释,non-volatile,到底是处于什么想法呢?
作者是如何保证outcome对其他线程的可见呢?

private Object outcome; // non-volatile, protected by state reads/writes
protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
}

这个时候我们必须想到java可见性一个很重要的规则happens-before


第一重解析

首先我想的了volatile传递性规则
也就是我考虑到state是volatile申明的,如果我们能够发现
hb(outcome=v , get outcome) 那么我们就可以得出 outcome一定可以被其他线程可见。

以下下翻译一个经典的例子(来源于引用1):

一段简写程序:

volatile int state;  

Integer result;

void succeed(Integer result)
    if(state==PENDING)              vr0
        this.result = result;        w1
        state = DONE;               vw1

Integer peekResult()
    if(state==DONE)                 vr2 
        return result;               r2
    return null;

如果state == DONE 那么该线程一定看到w1。
因为根据volatile规则可知 : hb(vw1,vr2)。 同时根据程序次序规则可知 : hb(w1,vw1), hb(vr2,r2)。
由此根据引用传递规则可以知道:
w1 -> vw1 -> vr2 -> r2
所以线程写w1时,对线程读r2是可见的。

然而succeed() 线程不安全,vr0到vw1不是原子性的,我们可以使用CAS。

void succeed(Integer result)
    if( compareAndSet(state, PENDING, DONE) )      vr0+vw0
        this.result = result;                       w1

这固然可以让vr0到vw1是原子性的,然而并不能让 w1对r2可读。
我们可以把以上方法拆分,变成以下结构。

void succeed(Integer result)
    if(state==PENDING)         vr0
        state=DONE;            vw0
        this.result = result;   w1

尽管可以知道 hb(vr0,vw0,w1),但是 无法保证hb(w1,r2)。
于是我们引入了一个中间变量 TMP。

void succeed(Integer result)
    if(state==PENDING)            vr0
        state=TMP;                vw0
        this.result = result;      w1
        state=DONE;               vw1

这样hb(w1,vw1), hb(vw1,vr2),所以hb(w1,r2)。
我们将以上转换成CAS

void succeed(Integer result)
    if( compareAndSet(state, PENDING, TMP) )       vr0+vw0
        this.result = result;                       w1
        state=DONE;                                vw1

回到FutureTask类来

我们把关键结构提出来

private volatile int state;
private Object outcome;

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}
private int awaitDone(boolean timed, long nanos) {          
        xxxxxxx

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        xxxxxxx
    }
}

可以看到,源码中的结构几乎和实例上的结构一模一样,经典的通过传递规则来实现 outcome可见性。
然而遗憾的作者"画蛇添足"的一行代码:

UNSAFE.putOrderedInt(this, stateOffset, NORMAL);

参见引用2。我们可以得知:putOrderedXXX方法是putXXXVolatile方法的延迟实现,不保证值的改变被其他线程立即看到。
不保证其他线程立刻看到,也就不符合happens-before里的volatile变量规则,也就不具有传递规则。我们可以等价于以下代码。

volatile int state;
int tmp;
Object outcome;

void set(){
   if( compareAndSet(state, COMPLETING, TMP) )
        outcome = v;
        tmp = NORMAL;
}
void awaitDone(){
    if(state==COMPLETING && tmp==NORMAL){
        //xxxx
    }
}

很显然,其他线程未必就可见tmp,所以我们不能认为outcome一定对其他线程可见

第二重解析

尽管上面的解析无法证明出outcome对其他get方法线程一定可见,但是我们可以得出两个结论。

  1. 调用get方法线程一定知道state已经从NEW变成COMPLETING
    UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)
  2. 根据程序次序规则,当get方法线程知道state==NORMAL时,outcome=v一定对该线程可见。

带着结论我们从新代码。

private int awaitDone(boolean timed, long nanos) {
    xxxx

    int s = state;
    if (s > COMPLETING) {
        if (q != null)
            q.thread = null;
        return s;
    }
    else if (s == COMPLETING) // cannot time out yet
        Thread.yield();
    xxxx
}

可以明显的看到,当state还处于COMPLETING状态时,线程会让出cpu。一直wait到线程状态改变。
事实上x86底层就有多核同步缓存的协议,也就是即使没有volatile,状态也最终会同步。

由此我们终于搞清楚了,作者是利用程序次序规则+核同步缓存的协议,来最终保证outcome变量被调用get方法线程可见。

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

推荐阅读更多精彩内容