• 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实例是一个半成品,成员属性还没初始化

    解决方案:

    附加一些信息:

     

     

  • 相关阅读:
    贝叶斯定理经典案例
    java 简单秒杀
    menu JPopupMenu JTabbedPane
    java String matches 正则表达
    gg mirror
    后台计时
    css 标题
    ajax dataType
    jQuery ajax
    java null 空指针
  • 原文地址:https://www.cnblogs.com/yangxiaohui227/p/15509524.html
Copyright © 2020-2023  润新知