最近在阅读FutureTask的源码是发现了一个问题那就是源码中封装结果的字段并没有使用volatile修饰,源码如下:
public class FutureTask<V> implements RunnableFuture<V> {
/**
* 状态变化路径
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private static final int NEW = 0;
private static final int COMPLETING = 1;// 完成中
private static final int NORMAL = 2;// 正常结束
// 异常
private static final int EXCEPTIONAL = 3;
// 取消任务
private static final int CANCELLED = 4;
// 中断任务
private static final int INTERRUPTING = 5;
// 被中断的
private static final int INTERRUPTED = 6;
// 任务执行状态
private volatile int state;
// 待执行的任务
private Callable<V> callable;
// 封装的结果,或则执行的异常
private Object outcome; // non-volatile, protected by state reads/writes
// 执行当前任务的线程 通过CAS来设置
private volatile Thread runner;
// 所有等待获取执行结果的线程,被封装为一个链表数据结构
private volatile WaitNode waiters;
}
我们看到state字段是volatile修改的,但是outcome字段并没有volatile修饰。
继续看下这两个字段如何设置值的:
// 1. 正常结束设置结果
protected void set(V v) {
// 设置状态为完成中
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 设置结果
outcome = v;
// 设置状态为正常结束
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
// 后续事宜:唤醒等待的线程,调用done()方法
finishCompletion();
}
}
// 不带超时时间
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 状态小于等于完成中...(NEW,COMPLETING)
if (s <= COMPLETING)
// 等待
s = awaitDone(false, 0L);
//
return report(s);
}
咋一看,好像看不出什么名堂,这里能清楚得出结论的是state字段在get()方法中是可见的。
但是,outcome字段并没有volatile修饰,不能直接得出outcome字段在get()方法中也是可见的这样的结论。
happen-before
要搞清楚这个问题,我们首先来复习下volatile关键字的作用:
Happens-Before原则:前面一个操作的结果对后续操作是可见的。
Happens-Before原则约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守Happens-Before原则。即使编译器进行指令重排序的优化,如果结果和重排序前一致,也是允许的。
java1.5之后,通过happen-before原则增强了volatile关键词。volatile关键词是轻量的实现线程安全的方法,保证了volatile变量的有序性和可见性。
可见性保证
当写 volatile 变量时,JMM 会立即把该线程对应的本地内存中的共享变量值刷新到主内存。
当读 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile 保证内存可见性,其实是用到了 CPU 保证缓存一致性的 MESI 协议。当某线程对 volatile 变量的修改会立即回写到主存中,并且导致其他线程的缓存失效,强制其他线程再使用变量时,需要从主存中读取。
![](https://img-blog.csdnimg.cn/img_convert/2bfbddfe02aceadd35806b7cd10ddda3.png)
编译器有以下规则:
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
接下来我们来分析案例,对于如下代码:
private volatile int state;
private Object outcome;
public void set(Object v){
if(state == NEW){ // 1
outcome = v; // 2
state = DONE; // 3
}
}
public void get(){
if(state == DONE){ // 4
return outcome; // 5
}
return null;
}
根据volatile的happen-before原则,2对3是可见的,同时4对5是可见的,并且3对4是可见的,那么根据传递性: 2 < 3 < 4 < 5,我们不难得出,2 < 5成立,即2对5可见。
这里还有个问题就是多线程调用set()方法情况下存在竞争,我们继续改进set()方法。
private volatile int state;
private Object outcome;
public void set(Object v){
if(compareAndSet(state,NEW,DONE)){ // 1
outcome = v; // 2
}
}
public void get(){
if(state == DONE){ // 4
return outcome; // 5
}
return null;
}
这里解决了修改state字段的原子性,但是并不能保证刚才的2对5可见了,因为这里满足1对2可见,4对5可见,同时1对4可见,这里我们没法办推到出2对5可见。
继续修改,为了保证2对5的可见性,我们还是得保留3这一行代码。
那么我们完全可以增加一个中间临时变量TMP,代码就改成这样:
private volatile int state;
private Object outcome;
public void set(Object v){
if(compareAndSet(state,NEW,TMP)){ // 1
outcome = v; // 2
state = DONE; // 3
}
}
public void get(){
if(state == DONE){ // 4
return outcome; // 5
}
return null;
}
这样我们既保证了设置state字段的原子性,同时保证了outcome字段对get()方法的可见性。
这完全就是FutureTask中outcome的实现逻辑,所以我们已经正确分析了outcome为什么可以不加volatile关键字,也能保证可见性的原因。