zoukankan      html  css  js  c++  java
  • volatile

    首先看看如下代码以及结果:(计算一秒钟内count++的结果)

    public class VolatileDemo {
        private static Boolean flag = true;   //创建一个状态变量flag
        public static void main(String[] args) {
            Thread thread = new Thread(()-> {   //创建一个线程
                int count = 0;
               while (flag){
                   count++;
               }
               System.out.println("count:"+count);
            });
            Thread thread1 = new Thread(()->{//创建一个线程
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
                System.out.println("1秒结束");
            });
    
            thread1.start();
            thread.start();
        }
    }

     由运行结果可见,程序一直未停下来,说明变量flag修改无法实时同步给另一个线程。

    当加入volatile关键字修饰状态变量flag:

    public class VolatileDemo {
        private volatile static Boolean flag = true;
        public static void main(String[] args) {
            Thread thread = new Thread(()-> {
                int count = 0;
               while (flag){
                   count++;
               }
               System.out.println("count:"+count);
            });
            Thread thread1 = new Thread(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
                System.out.println("1秒结束");
            });
    
            thread1.start();
            thread.start();
        }
    }

     

     为什么会这样?

           在不加volatile关键时,线程堆栈中保存线程运行时的变量值的副本,当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。

    volatile的特征

    • 禁止重排序    代码在执行过程中,为了提高代码的执行效率,会对代码做优化,编译、字节码,机器码、汇编都会对代码进行优化,代码优化的原则是优化前后是不会来影响执行结果;Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远是按照其出现顺序执行的。重排序的依据是happens-before法则。
    • 保证内存的可见性   

     工作原理:

            在volatile关键字所修饰的变量时,在汇编层代码上,会添加一个lock前缀的指令,Lock前缀指令相当于添加了一个内存屏障,内存屏障提供的功能:

    1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

    2、它会强制将对缓存的修改操作立即写入主存;

    3、如果是写操作,它会导致其他CPU中对应的缓存行无效。

    禁止指令重排序的原理

           通过内存屏障实现,StoreStoreStoreLoadLoadLoadLoadStore屏障,保证写与写、写与读、读与读、读与写等有序。

    可见性原理

           解决内存一致性问题:JVM向处理器发送一个Lock前缀的指令,两种解决方案:

    第一种:通过总线加LocK锁前缀的方法锁定总线。

    第二种:通过缓存一致性协议(MESI协议)(缓存行 维护两个状态位 M,E,S,I)。

    M(被修改的):处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

    E(独占的):处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。

    S(共享的):处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

    I(无效的):本CPU中的这份缓存已经无效。

    缓存一致性协议执行过程:

          一个处于E (独占的)状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S;一个处于S (共享的)状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。一个处于M (被修改的)状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。

            当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取(无效的就要读取新值),并把自己状态变成S (既然是无效那么说明已有cpu对缓存做了修改),如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

    普通变量:

    1,变量值从主内存(在堆中)load到本地内存(在当前线程的栈桢中);

    2,之后,线程就不再和该变量在主内存中的值由任何关系,而是直接操作在副本变量上(这样速度很快),这时,如果主存中的count或本地内存中的副本发生任何变化,都不会影响到对方,也正是这个原因导致并发情况下出现数据不一致;

    3,在修改完的某个时刻(线程退出之前),自动把本地内存中的变量副本值回写到对象在堆中的对应变量。

    volatile修饰的变量

    volatile仍然在执行一个从主存加载到工作内存,并且将变更的值写回主存的操作,但是:

    1,volatile保证每次读取该变量前,都判断当前值是否已经失效(即是否已经与主存不一致),如果已经失效,则从主存load最新的变量;

    2,volatile保证每次对该变量做出修改时,都立即写入主存。

    注意:volatile保证共享数据的可见性,有序性,却无法保证数据的原子性。

    应用场景

    • 一般用来修饰Boolean类型的共享状态标志位。
    • 单例模式下的双重校验锁。
    • 修饰单个的变量。

    注意:volatile对于基本数据类型(值直接从主内存向工作内存copy)才有用。但是对于对象来说,似乎没有用,因为volatile只是保证对象引用的可见性,而对对象内部的字段,它保证不了任何事。即便是在使用ThreadLocal时,每个线程都有一份变量副本,这些副本本身也是存储在堆中的,线程栈桢中保存的仍然是基本数据类型和变量副本的引用。一个对象被volatile修饰,那么就表示它的引用具有了可见性。从而使得对于变量引用的任何变更,都在线程间可见,但是却不具备原子性。

  • 相关阅读:
    在maven项目中引用ueditor报错问题
    mysql主从复制
    Nginx+tomcat负载均衡配置
    SSM框架——使用MyBatis Generator自动创建代码
    mysql定时任务用到存储过程和定时任务
    ajax跨域请求のJSONP
    mysql中把一个表的数据批量导入另一个表中
    在Mysql中查询两个时间段的差,可以是秒,天,星期,月份,年...
    input框的输入限制
    java项目启动时执行指定方法
  • 原文地址:https://www.cnblogs.com/128-cdy/p/12501454.html
Copyright © 2011-2022 走看看