volatile是Java中一个非常重要的关键字,要想完全搞懂volatile的作用,需要一些额外的辅助知识。
要了解volatile,就要先对CPU,CPU cache和主存有所有了解。
主存就是我们平时说的内存,它的读写速度很慢,而CPU运算速度很快,为了弥补二者的差异,在CPU和主存之间引入了CPU Cache,
现在很多CPU都会有三层Cache,分别称为L1,L2和L3,其中L1最靠近CPU,缓存速度也最快,L1这块缓存被分为了两个部分,一部分是指令缓存,称为L1i,一部分是数据缓存,称为L1d.
只有有缓存,就会出现缓存不一致的问题,比如i++
这个操作。
在有缓存时候的计算流程是:
- 从主存中读取i的值到缓存中
- 在缓存中把i的值加1
- 把缓存中的值刷回到主存中。
这样的模型在单线程下没有任何问题,但是在多线程中,就会出现问题,比如在步骤二中执行,此时有一个线程直接给i重新赋值了,这时候就会出现问题。
解决这个问题的一个有效办法就是 缓存一致性协议,这个协议大体思路是:
- 当CPU操作Cache中的副本时,会去查看该副本是不是在其他Cache中也存在一份
- 如果不存在的话,CPU就可以对Cached中的副本进行任意的操作了
- 如果存在的话, CPU先把Cached中的副本读取到自己的寄存器中,然后通知其他的CPU说"你们保存的Cache无效了"
- 其他CPU收到这个通知之后,如果要操作这个变量的副本,就会从主存里在读取一遍。
那么我们接下来看一下Java的内存模型,Java的内存模型一个很重要的作用就是规定了:一个线程对共享变量(主内存中的变量)的修改,何时对其他线程可见。
具体来说就是:
- 每个线程都有自己的工作内存,称为缓存。
- 线程只能操作自己的工作内存,不能直接操作主内存
- 共享变量在工作内存中有一个副本
- JVM决定何时把工作内存中的副本刷回主内存。
接下来了解一下并发编程必须要掌握的几个概念:
原子性
一组操作要么不做,要么保证全部做完,不会被中断
volatile关键字无法保证原子性可见性
一个线程对一个变量进行了修改,另一个线程可以马上感知到这种修改。
volatile关键字可以保证可见性有序性
主要就是防止进行指令重排,因为为了优化执行顺序,JVM并不一定会按照书写的顺序去执行,会进行优化,优化的原则是:不保证执行的顺序,只保证重排之后的执行结果和重排之前一样。
在多线程情况下,指令重排有时候会造成问题。所以有时候要禁止掉指令重排。
最常见的就是通过一个变量来进行初始化判断,比如
private boolean isInit = false;
private Manager mManger;
public Manager getManager(){
if(!isInit){
mManger = initManager();
isInit = true;
}
return mManager;
}
在上面那个例子中,单线程情况下不会有任何问题,在多线程情况下,如果不进行指令重排也不会有问题。
但是如果进行了指令重排,比如指令重排之后,把isInit = true
放在了mManager = initManager()
之上,很可能在多线程的情况下出现mManager为空的情况,从而出现空指针异常。而且这种异常还很难发现,通常大家都是一脸懵逼,说mManager肯定不可能为空呀。
那现在我们把多线程情况下的指令重排造成的mManager为空的情况说一下。
线程1执行getManager方法,由于指令重排,isInit = true
先执行,然后去真正初始化mManager,最后返回mManager实例,不会有任何问题。
但是当线程1执行完isInit = true
之后,线程2开始执行getManager方法,发现isInit为true,就直接返回了mManager,而此时mManager还没有初始化,所以线程2中会出现空指针异常。
所以建议, 依赖一个变量来判断是否初始化,而且这种判断是运行在多线程下的,那么该变量一定要是volatile的
接下来我们看一下JVM是如何通过内存模型来保证原子性,可见性和有序性的。
JVM保证对于变量的读取和直接赋值都是原子性的,什么是直接赋值呢,比如x = 1
这就是直接赋值,JVM保证其原子性。但是y = x
就不是原子性的,因为这涉及到三个操作,第一步是把x的值读入工作内存,第二步是把x的值赋给y,第三步是把y的值刷回主内存。
同理类似于i++
,x=x+1
都不是原子性操作。
如果要确保这些操作的原子性,都要使用synchronized
关键字。
JVM通过synchronized
关键字,volatile
关键字和显示锁Lock来保证可见性。
JVM通过synchronized
关键字,volatile
关键字和显示锁Lock来保证有序性。
可见volatile关键字只能保证可见性和有序性。而使用锁的话则三个特性都能保证。
volatile的使用场景
由于volatile关键字可以保证可见性和有序性,并不能保证原子性,所以它不能完全替代锁。我们工作中对volatile的使用也是利用了它的可见性和有序性。
- 修饰开关变量。比如我们上面提到的例子
- 修饰单例变量,防止在单例模式中由于指令重排生成多个变量。
volatile关键字不会导致阻塞,但是使用锁会导致阻塞