写在前面
volatile关键字在面试中也算是高频问题了,基本上涉及到并发都会被问到这个问题。今天来简单的总结一下。
先说一下volatile关键字的作用
一. 禁止指令重排
何为禁止指令重排?用一个代码简单解释一下
public class Singleton {
//这里用volatile修饰的目的就是防止指令重排
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
//对于这一行代码,其实是分为三个部分执行的
//首先分配对象的内存空间
//初始化对象
//将对象指向刚分配的内存地址
//如果不用volatile关键字修饰,第二步和第三步可能会调换顺序
//假如有两个线程同时访问,线程A执行完第一步和第三步还未执行第二步,此时线程B认为sigleton已经初始化完毕,就会带来问题。
singleton=new Singleton();
}
}
}
return singleton;
}
}
实现方式
在JVM层面上,volatile内存区的读写都加上了内存屏障,用于实现对内存操作的限制。
二. 保证“共享变量”的可见性
何为可见性,为什么要保证可见性
首先说明一点,对于单核CPU来讲,是不存在可见性问题的,当然目前的计算机一般都是多核CPU,那么这就会带来可见性问题。例如说我们去执行几行java代码,那么最终这些代码是会交给我们的CPU去执行的,而代码中的一些数据是存放在内存中的。而CPU在去操作这些数据的时候,是不会直接操作内存的,因为每个CPU都有一个自己独立的缓存。例如在对数据进行修改时,首先会将数据从内存装载到缓存;接着在缓存中对数据进行修改;最后将修改后的数据刷新到内存中。
所以对于单核CPU是不存在可见性问题的,但如果是多核就会存在问题。例如我们有两个线程A和B去操作同一变量,线程A交给CPU1去执行,线程B交给CPU2去执行;接着CPU1将数据装载到自己的缓存中进行修改,CPU2将数据装载到自己的缓存中进行修改。最后都将修改后的数据刷新到缓存中。这里面就会出现一个问题,因为每个CPU的缓存都是独立的,CPU1是不知道其CPU2是否已经对数据进行了修改,假如说CPU2将变量由10修改为5,但是CPU2这个时候的缓存中仍然保存的是旧的数据,那么最终就会导致结果出现问题。
volatile 关键字保证可见性的实现方式
在对有volatile关键字修饰的变量进行写操作的时候,汇编后的代码会多出一个LOCK前缀指令,那么这个指令在多核CPU下会引发两件事情:
一. 立刻将当前CPU缓存行中的数据写回到系统内存。(强调一点,如果没有volatile关键字进行修饰,CPU在更新完数据以后是不会立刻将更新后的数据写会到内存中)
二. 这个写会内存的操作会导致其他CPU缓存了该内存地址的数据无效。在多核CPU下,每个CPU都会嗅探在总线上传播过来的数据,来检查自己的缓存是否过期,如果发现缓存行对应的内存地址被修改也就是过期了,就会将当前CPU缓存行设置为无效状态,当需要操作这个数据的时候,会重新从内存中把数据读到缓存行中。