volatile 的可见性,禁止指令重排序,无法保证原子性的理解

在了解volatile的原理前,我们先来看个示例代码:

public class Visualable {
    public static    boolean initFlag=false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            System.out.println("线程1开始执行");

            while (!initFlag){
              //nothing todo
             
            }

            System.out.println("线程执行1执行完毕");
        }).start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(()->{
            System.out.println("线程执行2开始执行");
            initFlag=true;
            System.out.println("线程执行2执行完毕");
        }).start();


    }
}

执行后显示:System.out.println("线程执行1执行完毕"); 这行代码没执行

 

 说明了线程2修改initFlag的值并没有让线程1感知到;为何?

这里我们就要了解Java线程内存模型了,在硬件级别来看:

 

 

 

 

 

 

以上图所示:一开始线程1和线程2都从主内存中将initFlag的值,读回了线程自己的内存中,此时线程2将值改了,但还没及时刷回主内存中,所以线程1感觉不到值的改变,因此线程1一直死循环,只要写

回了主内存,线程1就可以感觉到数据的改变,这个是由MSEI缓存一致性协议决定的,由各个cpu厂商实现,简单点说,每个线程都会监听总线,数据的变更要经过总线,然后,线程监听到变量变更后,会检查

自己的内存有没该变量,有就失效自己的变量,之后从主内存中读取,既然MSEI能保证内存的可见性,为何还会有问题,这是因为线程2没有及时将变量刷回主内存,而volatile可以保证变量的修改立即刷回主内存,因此保证可见性,因此代码修改一下:

 

 

 

 那为何volatile没法保证变量的原子性: 我们思考一个问题,如果一个变量 int sum=0;线程1和线程2都读取到内存中,然后都做了++操作,那么线程1如果先刷回内存中,线程2的内存变量就失效了,此时sum的值值加了1

那volatile的禁止指令重排又是啥?先看个demo

 

 为何会出现a=1 和b=1同时出现的情况,正常来说,a要等于1 证明y=1这行代码已经执行了,因为a=y,然而y=1执行了,证明b=x要先运行,那么b应该等于0才对,因为x此时等于0

出现这个结果的原因: 线程one中,a=y和x=1的代码顺序调换了,也就是指令重排了,线程Two b=x 和 y=1的代码顺序也重排了

什么时候会重排:遵循下面2个原则:

as-if-serial 语义:简单的说就是:能否重排,最重要的是,对于单线程来说,重排前后,结果不变,对于上面例子,对于线程one来说 a=y和x=1的顺序调换,结果是一样的,线程Two也是一样道理

happen-before 原则:

1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作
4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

这些规则来描述了什么时候可以重排

如何解决指令重排,很简单,只要在变量中加入volatile 关键字即可;

volatile 禁止重排原理:内存屏障,举个例子:

int a=1;

int b=2;

我们要系统禁止这2行代码进行重排,我们需要定义一个规范,就像产品经理说,我要实现某个功能,这是一种规范,真正实现由程序员去弄,现在假设我们的规范是,只要这两行代码中有abc三个字母就禁止重排

int a=1;

abc;

int b=2;

然后我们系统发现有abc就不会重排了,具体实现这个需求,是由jvm厂商去实现,如hotsport虚拟机:

内存屏障中的abc字母在hostport中有4种情况,读读屏障(对应的字母是 loadload),写写屏障(storestore),读写屏障(loadstore),写读屏障(storeload),汇编源码:

 

 

 指令重排在单例模式中的运用:

 

 上面经典的实现单例的双空判断,这个会有问题么?阿里规范也建议不要写成上面这种模式:

我们先看看这这段锁代码块的字节码:

 

由字节码知道:instance=new MyInstance() 分三步:

1. 先new 开辟内存空间

2. 执行 init方法,如果类有成员变量,如int a=9,此时会给a进行赋值

3. 将对象赋值给instance变量

如果指令没有重排,一切都没问题,假如指令重排了,如 2和3调换,先给instance赋值,再执行init初始化成员变量,如果instance赋值完毕后,另一个线程进来,发先instance实例不为空,然后就调用其中的成员属性,这样就有问题了,因为此时

的instance实例是一个半成品,成员属性还没初始化

解决方案:

 

 

附加一些信息:

 

 

 

 

 

posted @ 2021-11-04 18:17  yangxiaohui227  阅读(322)  评论(0编辑  收藏  举报