volatile
被称为轻量级的synchronized,运行时开销比synchronized
更小,在多线程并发编程中发挥着同步共享变量、禁止处理器重排序的重要做用。建议在学习volatie
以前,先看一下Java内存模型《什么是Java内存模型?》,由于volatile
和Java内存模型有着莫大的关系。html
在学习volatie
以前,须要补充下Java内存模型的相关(JMM)知识,咱们知道Java线程的全部操做都是在工做区进行的,那么工做区和主存之间的变量是怎么进行交互的呢,能够用下面的图来表示。java
read
操做传过来的变量值储存到工做区内存的变量副本中。store
操做传过来的值赋值给主存变量。这8
个操做每一个操做都是原子性的,可是几个操做连着一块儿就不是原子性了!c++
上面介绍了Java模型的8
个操做,那么这8
个操做和volatile
又有着什么关系呢。面试
什么是可见性,用一个例子来解释,先看一段代码,加入线程1
先执行,线程2
再执行编程
//线程1
boolean stop = false;
while (!stop) {
do();
}
//线程2
stop = true;
复制代码
线程1
执行后会进入到一个死循环中,当线程2
执行后,线程1
的死循环就必定会立刻结束吗?答案是不必定,由于线程2
执行完stop = true
后,并不会立刻将变量stop
的值true
写回主存中,也就是上图中的assign
执行完成以后,store
和write
并不会随着执行,线程1
没有当即将修改后的变量的值更新到主存中,即便线程2
及时将变量stop
的值写回主存中了,线程1
也没有了解到变量stop
的值已被修改而去主存中从新获取,也就是线程1
的load
、read
操做并不会立刻执行形成线程1
的工做区内存中的变量副本不是最新的。这两个缘由形成了线程1
的死循环也就不会立刻结束。
那么如何避免上诉的问题呢?咱们可使用volatile
关键字修饰变量stop
,以下bash
//线程1
volatile boolean stop = false;
while (!stop) {
do();
}
//线程2
stop = true;
复制代码
这样线程1
每次读取变量stop
的时候都会先去主存中获取变量stop
最新的值,线程2
每次修改变量stop
的值以后都会立刻将变量的值写回主存中,这样也就不会出现上述的问题了。多线程
那么关键字volatie
是如何作到的呢?volatie
规定了上述8
个操做的规则并发
load
时,线程才能对变量执行use
操做;只有线程的后一个操做是use
时,线程才能对变量执行load
操做。即规定了use
、load
、read
三个操做之间的约束关系,规定这三个操做必须连续的出现,保证了线程每次读取变量的值前都必须去主存获取最新的值。assign
时,线程才能对变量执行store
操做;只有线程的后一个操做是store
时,线程才能对变量执行assign
操做,即规定了assign
、store
、write
三个操做之间的约束关系,规定了这三个操做必须连续的出现,保证线程每次修改变量后都必须将变量的值写回主存。volatile
的这两个规则,也正是保证了共享变量的可见性。post
有序性即程序执行的顺序按照代码的前后顺序执行,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
语句是依赖于A
和B
的,若是按照这样的顺序去执行就不能保证结果不变了(违背了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
同时执行,线程1
的A
和B
的执行顺序多是A->B
或者B->A
(由于A和B之间没有依赖关系,能够指令重排序)。若是线程1
按照A->B
的顺序执行,那么线程2
执行后的结果s就是咱们想要的正确结果,若是线程1
按照B->A
的顺序执行,那么线程2
执行后的结果s可能就不是咱们想要的结果了,由于线程1
将变量stop
的值修改成true
后,线程2
立刻获取到stop
为true
而后执行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
关键字的变量的指令进行插入内存屏障来禁止特定类型的处理器重排序
咱们先看内存屏障有哪些及发挥的做用
StoreStore
屏障:禁止屏障上面变量的写和下面全部进行写的变量进行处理器重排序。StoreLoad
屏障:禁止屏障上面变量的写和下面全部进行读的变量进行处理器重排序。LoadLoad
屏障:禁止屏障上面变量的读和下面全部进行读的变量进行处理器重排序。LoadStore
屏障:禁止屏障上面变量的读和下面全部进行写的变量进行处理器重排序。再看volatile
是怎么插入屏障的
volatile
变量的写前面插入一个StoreStore
屏障。volatile
变量的写后面插入一个StoreLoad
屏障。volatile
变量的读后面插入一个LoadLoad
屏障。volatile
变量的读后面插入一个LoadStore
屏障。注意:写操做是在
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
关键字也只是保证了use
、load
、read
三个操做连在一块儿时候的原子性,还有assign
、store
、write
这三个操做连在一块儿时候的原子性,也就是volatile
关键字保证了变量读操做的原子性和写操做的原子性,而变量的自增过程须要对变量进行读和写两个过程,而这两个过程连在一块儿就不是原子性操做了。
因此说volatile
变量对于变量的单独写操做/读操做是保证了原子性的,而常说的原子性包括读写操做连在一块儿,因此说对于volatile
不保证原子性的。那么如何解决上面程序的问题呢?只能给increase
方法加锁,让在多线程状况下只有一个线程能执行increase
方法,也就是保证了一个线程对变量的读写是原子性的。固然还有个更优的方案,就是利用读写都为原子性的CAS
,利用CAS
对volatile
进行操做,既解决了volatile
不保证原子性的问题,同时消耗也没加锁的方式大
学完volatile
以后,是否是以为volatile
和CAS
有种似曾相识的感受?那它们之间有什么关系或者区别呢。
volatile
只能保证共享变量的读和写操做单个操做的原子性,而CAS
保证了共享变量的读和写两个操做一块儿的原子性(即CAS是原子性操做的)。volatile
的实现基于JMM
,而CAS
的实现基于硬件。Java并发编程:volatile关键字解析
JAVA并发六:完全理解volatile
Java内存模型与volatile
Java面试官最爱问的volatile关键字
原文地址:ddnd.cn/2019/03/19/…