volatile禁止重排序详解

首先说明本文并不是讲解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写的内存屏障

  1. 第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。所以在写操作前插入StoreStore屏障
  2. 第一个操作是volatile写时,第二个操作的volatile读/写操作都不能重排序。所以在写操作后插入StoreLoad屏障

好的,符合,那我们现在来推volatile读操作的内存屏障

第一个操作是volatile读操作时,不管第二个操作是什么,都不能重排序。而第二个操作则包括了读、写(不管是普通读写还是volatile读写)。

  • 所以对于后续的读操作,则应在volatile读后插入LoadLoad屏障
  • 对于后续的写操作,则应在volatile读后插入LoadStore屏障

其实到这里,就已经可以了,但我们可以反推否定是读前插入:因为第二个操作为volatile读时,对于普通读/写,是没有限制“NO”的,如图
在这里插入图片描述
正推跟反推,都证明了,LoadLoad是在读后插入的

本文到此完,吐槽一下笔者查了好多资料,居然没有相关文章,因此整理出来,希望能给各位一点帮助。

有误欢迎指出。

参考文章:
Java并发学习笔记 – Java中的Lock、volatile、同步关键字

volatile的指令重排序理解

指令重排序

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值