volatile特性
可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性。
从happens-before角度来看volatile的读写
从可见性来看,volatile的写-读,与锁的释放-获取具有相同的内存效果:volatile写和锁释放、volatile读与锁获取具有相同的内存效果。
class volatileTest{
int a = 0;
volatile boolean flag = false;
public void init(){
a = 1;//操作1
flag = true;//操作2
}
public void print(){
if(flag){
int j = a;//操作3
System.out.println(j++);//操作4
}
}
}
从happens-before来看:
- 程序顺序规则:1 happens-before 2,3 happens-before 4。
- volatile变量规则:2 happens-before 3。
- 传递性:1 happens-before 4。
黑色箭头表示程序顺序规则;
橙色箭头表示volatile规则;
蓝色箭头表示组合这些规则后提供的happens-before保证。
volatile读写的内存过程
volatile写的时候,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的时候,JMM会把该线程对应的本地内存置为无效后,线程接下来再从主内存中读取共享变量。
如何实现volatile小村效果?
因为JAVA、CPU会对代码、字节码执行重排序,因此这里就从编译器重排序、处理器重排序来分析。为了实现volatile内存效果,JMM会分别限制这2种类型的重排序类型。
JMM针对编译器制定的volatile重排序规则:
是否能重排序 | 第二个操作 | ||
第一个操作 | 普通读写 | Volatile读 | Volatile写 |
普通读写 |
|
| NO |
Volatile读 | NO | NO | NO |
Volatile写 |
| NO | NO |
从上表看出:
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序从而确保volatile写之前的操作不会被编译器排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作是什么,这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
JMM针对CPU重排序制定的volatile重排序规则:
编译器在生成字节码文件时会在指令序列中插入内存屏障。但对于编译器而言如何插入最少的内存屏障指令呢?对不起,无法确定,只能是本着“宁可错杀一千不可放过一个”来插入内存屏障指令:(优化是服从于JSR-133规范的,首先确保JSR-133规范,然后再谈优化)
- 分别在每个volatile写操作的前面插入一个StoreStore屏障、后面插入一个StoreLoad屏障。
- 分别在每个volatile读操作的后面依次插入一个LoadLoad屏障、一个LoadStore屏障。
volatile写
volatile写前面的StoreStore屏障指令能前面所有普通读写的结果刷新到主内存,这样所有的CPU中缓存的数据均一致。
volatile写后面的StoreLoad屏障指令则非常有趣,为什么是StoreLoad呢?因为考虑到volatile写后面可能与volatile读/写操作做重排序,或者,volatile写后面直接就是return的话(return之后就没有操作了必须对其他CPU可见,如果不是return的话,你也得必须对其他CPU可见),为了能及时刷新到主内存来确保所有CPU可见,JMM这里采用保守策略:在每个volatile写的后面、每个volatile读的前面插入一个StoreLoad屏障来达成volatile写的内存效果。
另外还可以从一个常见场景来做解释:JMM选择在每个volatile写的后面插入一个StoreLoad屏障指令,是因为常常是一个写线程写volatile变量,N个读线程读同一个volatile变量,当N 大大超过写线程时(此时写线程就1个而已),通过在volatile写后面的StoreLoad来确保写的结果及时刷新到主内存进而提高执行效率。
volatile读
LoadLoad屏障指令确保禁止CPU把volatile读与下面的普通读做重排序。
LoadStore屏障指令确保禁止CPU把volatile读与下面的普通写做重排序。
(我觉得,这里可以总结出一句话:为了实现volatile读写的内存可见性,写必须确保其准备工作的可见性以及结果的可见性,读必须确保其优先性)
volatile发展历史
int a = 0;
volatile boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if(flag){
int i = a;
......
}
}
JSR-133旧内存模型中,不允许volatile变量之间重排序但允许volatile变量与普通变量重排序,对于上述代码中可能存在如下情况:
Writer方法中a和flag不存在数据依赖性,因此,重排序可能出现的情况是:flag=true;a=1;
那么假如A线程执行writer方法,B线程执行reader方法,那么显而易见,结果不是我们想要的结果。
为了避免这个问题发生,JSR-133在新内存模型中对volatile进行了强化:
严格限制JMM和CPU对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。