美文网首页
关于volatile

关于volatile

作者: 34sir | 来源:发表于2018-02-22 15:31 被阅读23次

    建议先看Java内存模型

    作用

    一个变量被volatile修饰之后即具有两层意义:

    • 一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
    • 禁止进行指令重排序

    是否保证可见性?

    可以
    老规矩,看一个栗子:

    //线程1
    boolean canDo = false;
    while(!canDo){
        do();
    }
     
    //线程2
    canDo = true;
    

    基于对内存模型的了解做一下简单的分析:
    线程2会先copy一份canDo的值到工作内存,修改了值后并没有立即刷新到主存,这时可能出现线程2意外被终止,而线程1看不到canDo的最新值,那么就会陷入死循环,这显然不是我们想要的结果,于是这就出现了volatile的戏份了

    canDovolatile修饰后会产生如下的变化:

    • canDo的值修改后会立即刷新到主存
    • 当线程2进行修改时,会导致线程1的工作内存中缓存变量canDo的缓存行无效
    • 线程1再次读取canDo值时,由于缓存行无效会直接从主存读取

    上述三点体现出了立即可见

    是否保证原子性?

    不可以
    看栗子:

    public volatile int vol = 0;
         
        public void add() {
            vol++;
        }
         
        public  void test() {
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            add();
                    };
                }.start();
            }
        }
    

    我们期望的结果是:10*1000,但是结果往往偏小
    我们做一下分析:
    vol++这种自增操作显然不满足原子性,那么就有可能出现线程1刚将vol值读到工作内存还没执行自增操作,虽然volvolatile所修饰,但主存中它的值依然是100(假设此时值为100),此时线程2已经执行自增操作,所以截止到线程2执行完成vol的值是101,而不是我们期望的两次自增后的102
    所以结论是volatile无法保证原子性

    如何保证原子性?

    三种方式:

    • synchronized
    • Lock
    • AtomicInteger

    synchronized方式:

     public synchronized void add() {
            vol++;
        }
    

    Lock方式:

    public  void add() {
            lock.lock();
            try {
                vol++;
            } finally{
                lock.unlock();
            }
        }
    

    AtomicInteger方式:

     public  AtomicInteger vol = new AtomicInteger(); //java 1.5中出现的原子操作类
         
        public  void add() {
            vol.getAndIncrement();
        }
    

    这里对AtomicInteger做一下解释:
    在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作

    是否保证有序性?

    答案是,不能确保有序性,但可以一定程度上保证有序性
    由上文我们可以知道volatile可以禁止指令重排,那这里就有两层含义:

    • 程序执行到volatile的读或者写操作时,其之前的操作肯定全部完成并且结果对后面的可见,其后面的操作还没开始执行(这里的之前之后是指代码的前后顺序)
    • 访问volatile变量的语句不能前移也不能后移

    结合一个栗子来理解:

    x=0; //语句1
    y=1;//语句2
    vol=true; //语句3  vol是volatile变量
    x=1; //语句4
    y=0; //语句5
    

    语句3不能移到1,2之前也不能移到4,5之后,但是1,2和4,5之间的顺序可以调换

    看一个栗子来理解volatile确保有序性的价值:

    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2
     
    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);
    

    倘若代码中inited不是volatile变量,那么就会存在一个问题:
    指令重排后有可能语句2在1之前执行,那么doSomethingwithconfig就有可能在context = loadContext()之前执行,就会出现空指针
    如果initedvolatile变量,语句1必定在2之前执行,这样就避免了上述问题

    原理

    volatile是如何保证可见性和禁止指令重排的?

    “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

    lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    • 确保指令重排序时不会把其后面的指令排到内存屏障之前,也不会把前面的指令排到内存屏障的后面
    • 强制将对缓存的修改操作立即写入主存
    • 如果是写操作,它会导致其他CPU中对应的缓存行无效

    使用场景

    由上文我们可知synchronized是可以实现volatile所能实现的功能的,那么volatile对比synchronized有什么不同?
    答案:volatile性能要优于synchronized但是无法保证操作的原子性

    所以使用volatile需要满足两个条件:

    • 对变量的写操作不依赖于当前值
    • 该变量没有包含在具有其他变量的不变式中

    上述两个条件简单的说就是需要保证操作的原子性

    开发中常见的使用场景:

    • 上文提到过的状态表计量
    • 单例的double check
    class Singleton{
        private volatile static Singleton instance = null;
         
        private Singleton() {
             
        }
         
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    

    相关文章

      网友评论

          本文标题:关于volatile

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