【并发重要原则】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方法线程可见。

推荐阅读更多精彩内容