美文网首页
Java☞volatile理解

Java☞volatile理解

作者: 小明今晚加班 | 来源:发表于2019-06-19 10:48 被阅读0次

Java原子性、可见性、有序性

原子性:指一个操作或多个操作要么全部执行,且执行过程不会被任何因素打断,要么就都不执行.
处理器实现原子操作的两种方式:

  1. 使用总线锁保证原子性
    如果多个处理器同时对共享变量进行读写操作(如 i++),共享变量就会被多个处理器同时进行操作,这样的读写操作就不是原子的。我们要做的就是当cpu1读写共享变量时,cpu2不能对该共享变量进行操作。处理器使用总线锁就是解决这个问题的,总线锁就是使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其它处理器的请求被阻塞,那么该处理器就可以独占该共享内存。
    Java的锁机制其实就是锁总线。
  2. 使用缓存锁保证原子性
    总线锁把CPU和内存之间通信锁住,使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大。所以在某些场合使用缓存锁替代总线锁。
    缓存锁是指通过锁住CPU缓存,在CPU缓存区实现共享变量的原子性操作。这个过程通过缓存一致性机制来保证操作的原子性,因为缓存一致性机制阻止多个处理器同时修改缓存的内存区域数据【缓存一致性机制整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取】。缓存锁使用的是比较和交换策略(Compare And Swap简称CAS),CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
    java并发包中的原子类就是使用缓存锁机制。

可见性:当一个线程修改了共享变量的值,其它线程能够立即得知这个修改.
普通变量和volatile变量的区别:volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。我们可以说volatile保证了线程操作时变量的可见性。
Java中提供了volatile关键字,通过volatile关键字修饰内存中的变量,该变量在线程之间共享;同时volatile关键字是轻量级的锁(synchronized),在使用的时候,消耗的成本比synchronized小很多。
volatile实现原理:
volatile修饰的变量,在翻译成汇编语言的时候,会有一个LOCK前缀的指令,LOCK前缀的指令在多核处理器下会引发两件事情:
1)、将当期处理器缓存行的数据会写回到系统内存
2)、这个写回内存的操作会引起在其他CPU里缓存该内存地址的数据无效
如果对声明了volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

有序性:即程序执行的顺序按照代码的先后顺序执行.
有序性的语意有几层,

  1. 最常见的就是保证多线程运行的串行顺序
  2. 防止重排序引起的问题
  3. 程序运行的先后顺序。比方JMM定义的一些Happens-before规则

synchronized保证有序性原理:JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。当使用synchronized关键字时,只能有一个线程执行直到执行完成后或异常,才会释放锁。所以可以保证synchronized代码块或方法只会有一个线程执行,保障了程序的有序性。
用单例模式的DCL写法来说明一下:

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

如果不加volatile会有什么问题呢?
java内存模型(JMM)并不限制处理器重排序,在执行instance=new Singleton();时,并不是原子语句,实际是包括了下面三大步骤:
1.为对象分配内存
2.初始化实例对象
3.把引用instance指向分配的内存空间
这个三个步骤并不能保证按序执行,处理器会进行指令重排序优化,存在这样的情况:
优化重排后执行顺序为:1,3,2, 这样在线程1执行到3时,instance已经不为null了,线程2此时判断instance!=null,则直接返回instance引用,但现在实例对象还没有初始化完毕,此时线程2使用instance可能会造成程序崩溃。
所以说,实际上synchronized只是保证了其有序性,并没有办法保证其原子性,而其可见性,是依靠java的内存模型来保证的。
总结:volatile可保证多线程操作的可见性,能在一定程度上保证有序性【volatile能禁止指令重排序,但是这里只能保证volatile所修饰的变量之前的程序不会在该变量之后执行,该变量之后的代码不会在变量之前执行】,但不能保证原子性。

相关文章

网友评论

      本文标题:Java☞volatile理解

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