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方法线程一定可见,但是我们可以得出两个结论。
- 调用get方法线程一定知道state已经从
NEW
变成COMPLETING
。
UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)
- 根据
程序次序规则
,当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方法线程可见。