• volatile关键字解析


    理解volatile关键字之前,建议先理解java内存模型(参考......)。

    在并发编程中,难免会遇到共享数据并发处理的问题,这些问题主要体现在了并发编程需要注意的几个特性:原子性,可见性,有序性(参考......)。volatile能够保证其中的可见性和有序性(一定程度上),但不能保证原子性。
    所以用volatile修饰一个共享变量A,那么对A就有了2层语义:
    1. 变量A在多线程处理中持有可见性,即线程1修改了变量A,那么其他线程可以立即看到变量A的新值。
    2. 禁止指令重排序(一定程度有序性的体现)。
    下面我们分别理解下这2层语义:
    可见性:
    下面我们先看一段简单的代码:
    boolean stop = false;
    
    // 线程1
    while(stop ) {
         working();
    }
    // 线程2
    stop = true ;
    编写这段代码的本意是通过stop变量作为中断标志,控制线程1是否执行继续执行某个任务。理想的情况是线程2执行stop true 后,线程1在执行完当前working()的任务后,立刻退出 while 循环。但是事实不太理想。这段代码有很大的不确定性。根据java内存模型的相关知识,我们知道线程1和2共享stop变量(在主内存中),并且在线程各自的缓存中有stop变量的副本。线程2执行stop true ,计算机对该指令实际操作有:从主内存读取stop变量保存到自己的工作内存(缓存)修改工作内存中的stop变量副本值为true 将工作内存中stop变量副本值(true)更新到主内存的共享stop变量中。
    但这线程2有可能在执行完第  条指令之后转去做其他事情,第  条指令迟迟未执行。线程1只能不断的working,working,working......直到线程2的第  条指令执行。

    如果stop变量声明为:public volatile boolean stop = false就能达到我们用stop变量作为中断标志的本意。使用volatile修饰符对这段代码有以下影响:
    1. 线程2对stop变量修改后会立即将新值更新到主内存;
    2. 线程2对stop变量修改,会导致线程1工作内存中的stop缓存变量的缓存行失效 (反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
    3. 由于线程1的工作内存中的stop缓存变量的缓存行无效了,所以线程1再次获取stop的值时会从主内存读取。
    这样线程2对stop变量作修改,在线程1中就能立刻更新到新值,并停止working,working,working......

    禁止指令重排序:
    这里涉及到指令重排序的概念,请参考......
    volatile关键字的禁止指令重排序体现在:
    1. 当程序执行到用volatile修饰的变量的读/写操作时,在这个操作前面的其他操作肯定已经完成,且结果对后面的其他操作(肯定还没有执行)可见。
    2. 在处理器对代码指令进行优化时,不能将对volatile变量操作前的指令放在其后面执行,也不能将对volatile变量操作后的指令放在其前面执行。
    先举个简单的例子:
    int x ;
    int y ;
    volatile int z ;
    
    x = 0;        // 语句1
    y = 0;        // 语句2
    
    z = 666;      // 语句3
    
    x = 1;        // 语句4
    y = 1;        // 语句5

    由于 z 变量被volatile修饰,那么处理器对指令重排序的时候,不会将语句3放在语句1,语句2前面,也不会将语句3放在语句4,语句5后面。但语句1,语句2的执行顺序、语句4,语句5的执行顺序有可能乱序(因为执行顺序修改后结果是一致的)。并且volatile关键字能保证程序执行到语句3时,语句1,语句2肯定已经执行结束,语句4,语句5未执行,而且语句1,语句2的执行结果对语句4,语句5可见。

    再举个用一个共享变量做多线程信号标记的例子:
    // 线程1
    context = initContext();   // 语句1
    inited = true;             // 语句2
    
    //线程2:
    while(!inited ){
           sleep()
    }
    doSomethingwithconfig(context);

    由于指令重排序,语句2有可能在语句1之前执行,那么线程2有可能用一个null的context去执行doSomethingwithconfig方法而导致程序出错。
    如果inited变量用volatile修饰,那么就可以保证语句1语句2之前执行,就能保证线程2正确执行。


    实现原理:
    前面讲述了源于volatile关键字的概念和使用,那么java虚拟机如何对volatile变量实现可见性和禁止指令重排?

    下面这段话摘自《深入理解Java虚拟机》:

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

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

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

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

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

    使用场景:
    在某些情况下,如果读操作远远大于写操作,volatile 变量可以提供优于其他锁的性能优势。
    但是要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
    1. 对变量的写操作不依赖于当前值。
    2. 该变量没有包含在具有其他变量的不变式中。

    以下是几个volatile常用的场景:
    状态标识:
    volatile boolean flag = false;
    
    while(!flag){
        doSomething ();
    }
    
    public void setFlag() {
        flag = true;
    }
    
    
    volatile boolean inited = false;
    //线程1:
    context = initContext() ; 
    inited = true;           
     
    //线程2:
    while(!inited ){
    sleep( )
    }
    doSomethingwithconfig(context);

    double check(参考http://www.iteye.com/topic/652440):
    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在多线程应用中并不是完全的线程安全(不能完全保证原子性,可见性,有序性),所以不能用volatile替代lock,synchronized之类的锁。
    volatile不足以确保类似多线程进行count++操作的原子性,除非能保证只有一个线程进行count++。


    引申:
    volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

    参考资料:
    《java并发编程实践》
  • 相关阅读:
    程序员最好也要懂的一些沟通说服技巧
    MapFields和并行计算(OpenFOAM)
    Python 调用自己编写的Class
    Ubuntu 图形桌面死机重启(机器不重启)
    Ubuntu 安装 Visual Studio Code
    Ubuntu图形界面和终端界面切换快捷键
    Linux直接在通过终端打开图片文件
    Linux新建环境变量快速切换到文件夹(export)
    Python文件读写(一)
    Dictonary(Python)(一)
  • 原文地址:https://www.cnblogs.com/ViviChan/p/4981717.html
Copyright © 2020-2023  润新知