Java volatile关键字解析

image

volatile简介

volatile被称为轻量级的synchronized,运行时开销比synchronized更小,在多线程并发编程中发挥着同步共享变量禁止处理器重排序的重要做用。建议在学习volatie以前,先看一下Java内存模型《什么是Java内存模型?》,由于volatile和Java内存模型有着莫大的关系。html

Java内存模型

在学习volatie以前,须要补充下Java内存模型的相关(JMM)知识,咱们知道Java线程的全部操做都是在工做区进行的,那么工做区和主存之间的变量是怎么进行交互的呢,能够用下面的图来表示。java

Java经过几种原子操做完成 工做区内存主存的交互

  1. lock:做用于主存,把变量标识为线程独占状态。
  2. unlock:做用于主存,解除变量的独占状态。
  3. read:做用于主存,把一个变量的值经过主存传输到线程的工做区内存。
  4. load:做用于工做区内存,把read操做传过来的变量值储存到工做区内存的变量副本中。
  5. use:做用于工做内存,把工做区内存的变量副本传给执行引擎。
  6. assign:做用于工做区内存,把从执行引擎传过来的值赋值给工做区内存的变量副本。
  7. store:做用于工做区内存,把工做区内存的变量副本传给主存。
  8. write:做用于主存,把store操做传过来的值赋值给主存变量。

8个操做每一个操做都是原子性的,可是几个操做连着一块儿就不是原子性了!c++

volatile原理

上面介绍了Java模型的8个操做,那么这8个操做和volatile又有着什么关系呢。面试

volatile的可见性

什么是可见性,用一个例子来解释,先看一段代码,加入线程1先执行,线程2再执行编程

//线程1
boolean stop = false;
while (!stop) {
    do();
} 

//线程2
stop = true;
复制代码

线程1执行后会进入到一个死循环中,当线程2执行后,线程1的死循环就必定会立刻结束吗?答案是不必定,由于线程2执行完stop = true后,并不会立刻将变量stop的值true写回主存中,也就是上图中的assign执行完成以后,storewrite并不会随着执行,线程1没有当即将修改后的变量的值更新到主存中,即便线程2及时将变量stop的值写回主存中了,线程1也没有了解到变量stop的值已被修改而去主存中从新获取,也就是线程1loadread操做并不会立刻执行形成线程1的工做区内存中的变量副本不是最新的。这两个缘由形成了线程1的死循环也就不会立刻结束。
那么如何避免上诉的问题呢?咱们可使用volatile关键字修饰变量stop,以下bash

//线程1
volatile boolean stop = false;
while (!stop) {
    do();
} 

//线程2
stop = true;
复制代码

这样线程1每次读取变量stop的时候都会先去主存中获取变量stop最新的值,线程2每次修改变量stop的值以后都会立刻将变量的值写回主存中,这样也就不会出现上述的问题了。多线程

那么关键字volatie是如何作到的呢?volatie规定了上述8个操做的规则并发

  1. 只有当线程对变量执行的前一个操做load时,线程才能对变量执行use操做;只有线程的后一个操做是use时,线程才能对变量执行load操做。即规定了useloadread三个操做之间的约束关系,规定这三个操做必须连续的出现,保证了线程每次读取变量的值前都必须去主存获取最新的值
  2. 只有当前程对变量执行的前一个操做assign时,线程才能对变量执行store操做;只有线程的后一个操做是store时,线程才能对变量执行assign操做,即规定了assignstorewrite三个操做之间的约束关系,规定了这三个操做必须连续的出现,保证线程每次修改变量后都必须将变量的值写回主存

volatile的这两个规则,也正是保证了共享变量的可见性post

volatile的有序性

有序性即程序执行的顺序按照代码的前后顺序执行,Java内存模型(JMM)容许编译器和处理器对指令进行重排序,可是规定了as-if-serial语义,即保证单线程状况下无论怎么重排序,程序的结果不能改变,如学习

double pi = 3.14;  //A
double r = 1;     //B
double s = pi * r * r; //C
复制代码

上面的代码可能按照A->B->C顺序执行,也有可能按照B->A->C顺序执行,这两种顺序都不会影响程序的结果。可是不会以C->A(B)->B(A)的顺序去执行,由于C语句是依赖于AB的,若是按照这样的顺序去执行就不能保证结果不变了(违背了as-if-serial)。

上面介绍的是单线程的执行,无论指令怎么重排序都不会影响结果,可是在多线程下就会出现问题了。
下面看个例子

double pi = 3.14;
double r = 0;
double s = 0;
boolean start = false;
//线程1
r = 10; //A
start = true; //B

//线程2
if (start) {  //C
    s = pi * r * r;  //D
}
复制代码

线程1和线程2同时执行,线程1AB的执行顺序多是A->B或者B->A(由于A和B之间没有依赖关系,能够指令重排序)。若是线程1按照A->B的顺序执行,那么线程2执行后的结果s就是咱们想要的正确结果,若是线程1按照B->A的顺序执行,那么线程2执行后的结果s可能就不是咱们想要的结果了,由于线程1将变量stop的值修改成true后,线程2立刻获取到stoptrue而后执行C语句,而后执行D语句即s = 3.14 * 0 * 0,而后线程1再执行B语句,那么结果就是有问题了。

那么为了解决这个问题,咱们能够在变量true加上关键字volatile

double pi = 3.14;
double r = 0;
double s = 0;
volatile boolean start = false;
//线程1
r = 10; //A
start = true; //B

//线程2
if (start) {  //C
    s = pi * r * r;  //D
}
复制代码

这样线程1的执行顺序就只能是A->B了,由于关键字发挥了禁止处理器指令重排序的做用,因此线程2的执行结果就不会有问题了。

那么volatile是怎么实现禁止处理器重排序的呢?
编译器会在编译生成字节码的时候,在加有volatile关键字的变量的指令进行插入内存屏障来禁止特定类型的处理器重排序
咱们先看内存屏障有哪些及发挥的做用

image

  1. StoreStore屏障:禁止屏障上面变量的写和下面全部进行写的变量进行处理器重排序。
  2. StoreLoad屏障:禁止屏障上面变量的写和下面全部进行读的变量进行处理器重排序。
  3. LoadLoad屏障:禁止屏障上面变量的读和下面全部进行读的变量进行处理器重排序。
  4. LoadStore屏障:禁止屏障上面变量的读和下面全部进行写的变量进行处理器重排序。

再看volatile是怎么插入屏障的

  1. 在每一个volatile变量的写前面插入一个StoreStore屏障。
  2. 在每一个volatile变量的写后面插入一个StoreLoad屏障。
  3. 在每一个volatile变量的读后面插入一个LoadLoad屏障。
  4. 在每一个volatile变量的读后面插入一个LoadStore屏障。

注意:写操做是在volatile先后插入一个内存屏障,而读操做是在后面插入两个内存屏障。

volatile变量经过插入内存屏障禁止了处理器重排序,从而解决了多线程环境下处理器重排序的问题

volatile有没有原子性?

上面分别介绍了volatile的可见性和有序性,那么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);
    }
}
复制代码

咱们开启10个线程对volatile变量进行自增操做,每一个线程对volatile变量执行1000次自增操做,那结果变量inc会是10000吗?答案是,变量inc的值基本都是小于10000
可能你会有疑问,volatile变量inc不是保证了共享变量的可见性了吗,每次线程读取到的都是最新的值,是的没错,可是线程每次将值写回主存的时候并不能保证主存中的值没有被其余的线程修过过

若是所示:线程1在主存中获取了i的最新值(i=1),线程2也在主存中获取了i的最新值(i=1,注意这时候线程1并未对变量i进行修改,因此i的值仍是1)),而后线程2将i自增后写回主存,这时候主存中i=2,到这里尚未问题,而后线程1又对i进行了自增写回了主存,这时候主存中i=2,也就是对i作了2次自增操做,结果i的结果只自增了1,问题就出来了这里。

为何会有这个问题呢,前面咱们提到了Java内存模型和主存之间交互的8个操做都是原子性的,可是他们的操做连在一块儿就不是原子性了,而volatile关键字也只是保证了useloadread三个操做连在一块儿时候的原子性,还有assignstorewrite这三个操做连在一块儿时候的原子性,也就是volatile关键字保证了变量读操做的原子性和写操做的原子性,而变量的自增过程须要对变量进行读和写两个过程,而这两个过程连在一块儿就不是原子性操做了。

因此说volatile变量对于变量的单独写操做/读操做是保证了原子性的,而常说的原子性包括读写操做连在一块儿,因此说对于volatile不保证原子性的。那么如何解决上面程序的问题呢?只能给increase方法加锁,让在多线程状况下只有一个线程能执行increase方法,也就是保证了一个线程对变量的读写是原子性的。固然还有个更优的方案,就是利用读写都为原子性的CAS,利用CASvolatile进行操做,既解决了volatile不保证原子性的问题,同时消耗也没加锁的方式大

volatile和CAS

学完volatile以后,是否是以为volatileCAS有种似曾相识的感受?那它们之间有什么关系或者区别呢。

  1. volatile只能保证共享变量的读和写操做单个操做的原子性,而CAS保证了共享变量的读和写两个操做一块儿的原子性(即CAS是原子性操做的)。
  2. volatile的实现基于JMM,而CAS的实现基于硬件。

参考

Java并发编程:volatile关键字解析
JAVA并发六:完全理解volatile
Java内存模型与volatile
Java面试官最爱问的volatile关键字

原文地址:ddnd.cn/2019/03/19/…

相关文章
相关标签/搜索