首先说明本文并不是讲解volatile不保证原子性、如何保证可见性xxxx,还不懂的请参考
让你彻底理解volatile
并发关键字volatile(重排序和内存屏障)
本文针对以下两个问题解答
1. 重排序规则中,volatile读写跟普通读写有什么关系,为什么要限制它们
2. volatile读操作的内存屏障的LoadLoad屏障到底是在读前还是读后
直接进入主题
重排序规则中,volatile读写跟普通读写有什么关系,为什么要限制它们
volatile变量的读写不能重排序很好理解,可是这跟普通读写有什么关系呢?普通读写是怎么影响到结果的?
首先明确volatile读/写的内存语义:
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。(注意:是所有的共享变量,不光是volatile变量)
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存 (注意:这里也是所有的共享变量)
即:无论是volatile读还是volatile写,都会刷新本地内存的所有共享变量,而不单是volatile变量
保证了在读volatile变量之前,内存中所有的共享变量都是最新的,也就是之前执行的的任意线程的写操作都对本次的读操作可见;
同样,本次写操作都对之后任意线程执行的读操作可见。
可能有读者觉得这样没什么必要,那我们来举例看看不这么限制的话会有什么后果
volatile int a = 0;
int b = 1;
public void A (){
b = 2; // 1 普通写
a = 1; // 2 volatile写
}
public void B() {
int c = 0;
if (a == 1) // 3 volatile读
c = b; // 4 普通写
}
假如volatile不影响重排序,那么由于代码1和代码2两处没有数据依赖性,所以二者是可以重排序的。
我们假设代码2在代码1之前被执行,此时由于a是volatile变量,所以将a = 1, b = 1刷新进入主内存;
如果这时候方法A所在的线程cpu时间片用完了,轮到了方法B在另一个线程中执行,由于a是volatile变量所以代码3处执行的时候会将b = 1, a = 1从主内存中读出,此时代码4再执行的话c会变为1,而不是预想的2(因为按照我们书写的顺序来看,a=1发生在b=2之后)
发生这种错误的原因在于:volatile变量写操作与在其之前的代码发生了重排序,使得刷新内存的时机提早了,可能会漏掉我们写在volatile变量赋值操作之前的那些共享变量的修改。
所以这就引出了volatile变量对指令重排序的第一个影响:
- 第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。确保volatile写操作刷新内存里共享变量的值时,程序员希望发生的变动都能够正确的刷新到内存中
而这例子也对应:
第一个操作为普通读/写,第二个操作为volatile写,也验证了确实是不能重排序,因为是可能会产生影响的。
PS:笔者建议此处应该按第二个操作去记忆
同理,再举个例子解释,第一个操作为volatile读,第二个操作为普通读/写 不能重排原因。
首先要明确happens-before中的猜测执行机制:
当存在控制依赖时,编译器和处理器会采取猜测执行机制来提高并行度
int a = 1;
boolean flag = true ;
if(flag){ //代码5
a * = 2; //代码6
}
代码5和6不存在数据依赖,可能会重排,处理器和编译器会先将代码6的执行结果放在缓冲区,等代码5判断为真时之后,将缓冲区的结果直接赋值给a,即实质上对操作5和6做了重排序
那么继续用上述例子,稍作修改
volatile int a = 1;
int b = 1;
public void A (){
a = 2; //1 volatile写
}
public void B() {
int c = 0;
if (a == 1) // 3 volatile读
c = b; // 4 普通写
}
现在换成先执行方法B。
基于上述的猜测执行机制,程序会把b=1先写到缓冲区,
如果这时候方法B所在的线程cpu时间片用完了,轮到了方法A在另一个线程中执行,把a赋值为2,然后返回方法B所在的线程,可是此时不满足判断条件,所以c不会被赋值为1,即c的值还是为0,而这显然是不对的。
同理,引出第二条规则:
- 第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。确保volatile读操作读取内存里的最新值是程序员希望读到的、操作的值
到此,规则中跟普通读写相关的“NO”已经解释完了
而volatile变量的读写间不能重排序就不举例了。
volatile读操作的内存屏障的LoadLoad屏障到底是在读前还是读后
在说volatile读之前,我们根据规则表,先来推volatile写的内存屏障
- 第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。所以在写操作前插入StoreStore屏障
- 第一个操作是volatile写时,第二个操作的volatile读/写操作都不能重排序。所以在写操作后插入StoreLoad屏障
好的,符合,那我们现在来推volatile读操作的内存屏障
第一个操作是volatile读操作时,不管第二个操作是什么,都不能重排序。而第二个操作则包括了读、写(不管是普通读写还是volatile读写)。
- 所以对于后续的读操作,则应在volatile读后插入LoadLoad屏障
- 对于后续的写操作,则应在volatile读后插入LoadStore屏障
其实到这里,就已经可以了,但我们可以反推否定是读前插入:因为第二个操作为volatile读时,对于普通读/写,是没有限制“NO”的,如图
正推跟反推,都证明了,LoadLoad是在读后插入的
本文到此完,吐槽一下笔者查了好多资料,居然没有相关文章,因此整理出来,希望能给各位一点帮助。
有误欢迎指出。