美文网首页
有关volatile关键字

有关volatile关键字

作者: ACtong | 来源:发表于2020-05-23 21:41 被阅读0次
    在Java虚拟机中volatile是什么?

    当一个变量被定义成volatile之后,它将具备两项特性:

    • 1、保证此变量对所有线程的可见性
    • 2、禁止指令重排序优化
    一、什么是对所有线程的可见性

    这里的“可见性”是指当一个线程修改了这个变量的值,新值对于其他线程来说是立即可知的。
    普通变量却做不到这一点,它的值在线程间传递时需要通过主内存完成。
    例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对 主内存进行读取操作,新变量值才会对线程B可见。

    基于volatile变量的运算在并发下并不能说是线程安全的,是由于Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。

    public class Test {
        public volatile int inc = 0;
         
        public void increase() {
            inc++;
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    

    以上代码执行的结果是10000吗?其实每次执行的结果都会不一样。
    由于使用了volatile关键字,不应该修改后马上更新回主存吗?
    确实使用了volatile关键字,当修改变量后,就会回到主存中,并且导致其他线程中的缓存无效,重新从主存中读取变量。
    但是inc++实际的操作是:
    从主存中读取inc变量,把inc自增一后,再写回主存,做了三步操作,那么就会出现以下情况:

    • 1、线程1先读取了inc的原始值0,然后线程1被阻塞了;
    • 2、线程2也去读取inc的原始值0,然后做自增操作后inc就等于1,写回到主存中。由于线程1只是读取了inc的值,并没有做写入操作,所以线程2的缓存也不会失效,直接做自增操作写回主存
    • 3、线程1在之前已经读取了inc的值,现在只是做修改操作,然后写回主存。这样就写入了两次11的值。

    由于volatile变量只能保证可见性,需要符合以下两条规则的运算场景中,否则我们要通过加锁 (使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:

    • 运算结果并不依赖变量的当前值,或者能确保只有单一线程修改变量的值
    • 变量不需要与其他的状态变量共同参与不变约束
    以下这类场景中就很适合使用volatile变量来控制并发
     volatile boolean shutdownRequested;
    
     public void shutdown() {
          shutdownRequested = true;
      }
    
     public void doWork() {
        while (!shutdownRequested) {
            // 代码的业务逻辑    
        }
     }
    

    当shutdown()方法被 调用时,能保证所有线程中执行的doWork()方法都立即停下来。

    一、什么是禁止指令重排序优化

    volatile能在一定程度保证有序性

    • 1、当程序执行到volatile变量的读或写操作时,在其前面的更改操作已经全部进行了,其结果对后面的操作可见
    • 2、在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    举个例子:

    //x、y为非volatile变量
    //flag为volatile变量
     
    x = 2;        //语句1
    y = 0;        //语句2
    flag = true;  //语句3
    x = 4;         //语句4
    y = -1;       //语句5
    

    由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

    总结

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

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

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

    • 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    • 2)它会强制将对缓存的修改操作立即写入主存;
    • 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

    最后,大多数场景下volatile的总开销仍然要比锁来得更低。

    参考文章

    1、《深入理解Java虚拟机》
    2、 Java并发编程:volatile关键字解析

    相关文章

      网友评论

          本文标题:有关volatile关键字

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