美文网首页
java并发中volatile的使用

java并发中volatile的使用

作者: HelloArmin | 来源:发表于2017-07-25 19:55 被阅读87次

    java中volatile声明变量,有两个作用

    • 保证变量对所有线程的可见性
    • 禁止指令重排

    保证可见性

    多线程访问共享变量时,声明volatile可以保证了共享变量可见性。可见性,即当一个线程修改共享变量时,另一个线程能读到变量修改后的值。

    没有了解过计算机系统低层实现的同学,可能会好奇,为什么会发生一个线程更新一个变量,另一线程读到的是是更新前的值的情况?

    计算机系统,为了提高处理速度,CPU其实并不直接与内存进行通信,而是将内存中的数据读取到CPU高速缓存(L1,L2,L3)中,操作完后也并不总是会马上写回内存。只有当缓存被置换出去的时候,才会去写内存。

    多处理器环境下,保证缓存一致性,有两种方式:总线锁和MESI缓存一致性协议。

    总线锁(bus locking):
    当一个CPU对缓存中的数据进行操作的时候,会往总线中发送一个Lock信号。这个时候,所有CPU收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的CPU就去内存中获取最新数据更新。

    总线锁带来的性能成本高昂,更多的计算机采用的是MESI缓存一致性协议。

    MESI协议, 会在缓存中存储一个标志位。这个标志位有以下四种状态:

    • M: Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中数据已经不一致了
    • E: Exclusive,独占缓存,当前CPU的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据
    • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段
    • I: Invalid,失效缓存,这个说明CPU中的缓存已经不能使用了

    每个CPU都会监听其他CPU对缓存的读写操作。

    • 当监听到其他CPU有读取操作,而自己的缓存处于M或E时,就将自己的缓存写入内存,并将自己状态修改为S。 - 当监听到其他CPU有写入内存操作,且缓存为S状态,则将状态职位I。
    • 当CPU的缓存状态是I,CPU将从内存中读取。

    使用volatile声明的变量,JVM会发送lock指令,将缓存写回内存。由于缓存一致性协议,写回缓存的时候,其他处理器的缓存将失效。从而保证了该变量值得一致性。

    禁止指令重排

    编译器和处理器为了提升程序性能,会按照一定的规则进行对指令进行重排顺序。在单线程下,这样的重排并不会带来问题。因为重排会遵守as-if-serial语义.

    为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。例如:

    
    double pi  = 3.14;    //A
    double r   = 1.0;     //B
    double area = pi * r * r; //C
    

    C 依赖于A 和B,因此重排时C不会被重排到A和B之前。而A和B没有依赖,重排也复合as-if-serial语义,编译器和处理器可以对其进行重排。

    经常提到的使用双检查锁来创建singleton对象的问题,正是由于指令重排导致的。

    public class Singleton {
    
      private static Singleton instance = null;
    
      private Singleton() { }
    
      public static Singleton getInstance() {
         if(instance == null) {
            synchronzied(Singleton.class) {
               if(instance == null) {
                   instance = new Singleton();  
               }
            }
         }
         return instance;
       }
    }
    
    

    new Singleton()并非是一个原子操作,它有多条指令组成:

    memory = allocate(); //1:分配对象的内存空间
    ctorInstance(memory); //2:初始化对象
    instance = memory; //3:设置instance指向刚分配的内存地址
    

    但是经过重排序后:

    memory = allocate(); //1:分配对象的内存空间
    instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
    ctorInstance(memory); //2:初始化对象
    

    指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。

    使用volatile会插入内存屏障指令,防止编译和运行时指令被重排。

    相关文章

      网友评论

          本文标题:java并发中volatile的使用

          本文链接:https://www.haomeiwen.com/subject/anjgkxtx.html