volatile是Java中一個(gè)非常重要的關(guān)鍵字窝剖,要想完全搞懂volatile的作用,需要一些額外的輔助知識(shí)朴读。
要了解volatile似踱,就要先對(duì)CPU,CPU cache和主存有所有了解。
主存就是我們平時(shí)說的內(nèi)存墓臭,它的讀寫速度很慢蘸鲸,而CPU運(yùn)算速度很快,為了彌補(bǔ)二者的差異窿锉,在CPU和主存之間引入了CPU Cache,
現(xiàn)在很多CPU都會(huì)有三層Cache,分別稱為L(zhǎng)1,L2和L3酌摇,其中L1最靠近CPU,緩存速度也最快嗡载,L1這塊緩存被分為了兩個(gè)部分窑多,一部分是指令緩存,稱為L(zhǎng)1i,一部分是數(shù)據(jù)緩存洼滚,稱為L(zhǎng)1d.
只有有緩存埂息,就會(huì)出現(xiàn)緩存不一致的問題,比如i++
這個(gè)操作遥巴。
在有緩存時(shí)候的計(jì)算流程是:
- 從主存中讀取i的值到緩存中
- 在緩存中把i的值加1
- 把緩存中的值刷回到主存中耿芹。
這樣的模型在單線程下沒有任何問題,但是在多線程中挪哄,就會(huì)出現(xiàn)問題吧秕,比如在步驟二中執(zhí)行,此時(shí)有一個(gè)線程直接給i重新賦值了迹炼,這時(shí)候就會(huì)出現(xiàn)問題砸彬。
解決這個(gè)問題的一個(gè)有效辦法就是 緩存一致性協(xié)議,這個(gè)協(xié)議大體思路是:
- 當(dāng)CPU操作Cache中的副本時(shí)斯入,會(huì)去查看該副本是不是在其他Cache中也存在一份
- 如果不存在的話砂碉,CPU就可以對(duì)Cached中的副本進(jìn)行任意的操作了
- 如果存在的話, CPU先把Cached中的副本讀取到自己的寄存器中刻两,然后通知其他的CPU說"你們保存的Cache無效了"
- 其他CPU收到這個(gè)通知之后增蹭,如果要操作這個(gè)變量的副本,就會(huì)從主存里在讀取一遍磅摹。
那么我們接下來看一下Java的內(nèi)存模型滋迈,Java的內(nèi)存模型一個(gè)很重要的作用就是規(guī)定了:一個(gè)線程對(duì)共享變量(主內(nèi)存中的變量)的修改,何時(shí)對(duì)其他線程可見户誓。
具體來說就是:
- 每個(gè)線程都有自己的工作內(nèi)存饼灿,稱為緩存。
- 線程只能操作自己的工作內(nèi)存帝美,不能直接操作主內(nèi)存
- 共享變量在工作內(nèi)存中有一個(gè)副本
- JVM決定何時(shí)把工作內(nèi)存中的副本刷回主內(nèi)存碍彭。
接下來了解一下并發(fā)編程必須要掌握的幾個(gè)概念:
原子性
一組操作要么不做,要么保證全部做完,不會(huì)被中斷
volatile關(guān)鍵字無法保證原子性可見性
一個(gè)線程對(duì)一個(gè)變量進(jìn)行了修改庇忌,另一個(gè)線程可以馬上感知到這種修改舞箍。
volatile關(guān)鍵字可以保證可見性有序性
主要就是防止進(jìn)行指令重排,因?yàn)闉榱藘?yōu)化執(zhí)行順序皆疹,JVM并不一定會(huì)按照書寫的順序去執(zhí)行创译,會(huì)進(jìn)行優(yōu)化,優(yōu)化的原則是:不保證執(zhí)行的順序墙基,只保證重排之后的執(zhí)行結(jié)果和重排之前一樣软族。
在多線程情況下,指令重排有時(shí)候會(huì)造成問題残制。所以有時(shí)候要禁止掉指令重排立砸。
最常見的就是通過一個(gè)變量來進(jìn)行初始化判斷,比如
private boolean isInit = false;
private Manager mManger;
public Manager getManager(){
if(!isInit){
mManger = initManager();
isInit = true;
}
return mManager;
}
在上面那個(gè)例子中初茶,單線程情況下不會(huì)有任何問題颗祝,在多線程情況下,如果不進(jìn)行指令重排也不會(huì)有問題恼布。
但是如果進(jìn)行了指令重排螺戳,比如指令重排之后,把isInit = true
放在了mManager = initManager()
之上折汞,很可能在多線程的情況下出現(xiàn)mManager為空的情況倔幼,從而出現(xiàn)空指針異常。而且這種異常還很難發(fā)現(xiàn)爽待,通常大家都是一臉懵逼损同,說mManager肯定不可能為空呀。
那現(xiàn)在我們把多線程情況下的指令重排造成的mManager為空的情況說一下鸟款。
線程1執(zhí)行g(shù)etManager方法膏燃,由于指令重排,isInit = true
先執(zhí)行何什,然后去真正初始化mManager,最后返回mManager實(shí)例组哩,不會(huì)有任何問題。
但是當(dāng)線程1執(zhí)行完isInit = true
之后处渣,線程2開始執(zhí)行g(shù)etManager方法伶贰,發(fā)現(xiàn)isInit為true,就直接返回了mManager霍比,而此時(shí)mManager還沒有初始化幕袱,所以線程2中會(huì)出現(xiàn)空指針異常暴备。
所以建議悠瞬, 依賴一個(gè)變量來判斷是否初始化,而且這種判斷是運(yùn)行在多線程下的,那么該變量一定要是volatile的
接下來我們看一下JVM是如何通過內(nèi)存模型來保證原子性浅妆,可見性和有序性的望迎。
JVM保證對(duì)于變量的讀取和直接賦值都是原子性的,什么是直接賦值呢凌外,比如x = 1
這就是直接賦值辩尊,JVM保證其原子性。但是y = x
就不是原子性的康辑,因?yàn)檫@涉及到三個(gè)操作摄欲,第一步是把x的值讀入工作內(nèi)存,第二步是把x的值賦給y,第三步是把y的值刷回主內(nèi)存疮薇。
同理類似于i++
,x=x+1
都不是原子性操作胸墙。
如果要確保這些操作的原子性,都要使用synchronized
關(guān)鍵字按咒。
JVM通過synchronized
關(guān)鍵字迟隅,volatile
關(guān)鍵字和顯示鎖Lock來保證可見性。
JVM通過synchronized
關(guān)鍵字励七,volatile
關(guān)鍵字和顯示鎖Lock來保證有序性智袭。
可見volatile關(guān)鍵字只能保證可見性和有序性。而使用鎖的話則三個(gè)特性都能保證掠抬。
volatile的使用場(chǎng)景
由于volatile關(guān)鍵字可以保證可見性和有序性吼野,并不能保證原子性,所以它不能完全替代鎖两波。我們工作中對(duì)volatile的使用也是利用了它的可見性和有序性箫锤。
- 修飾開關(guān)變量。比如我們上面提到的例子
- 修飾單例變量雨女,防止在單例模式中由于指令重排生成多個(gè)變量谚攒。
volatile關(guān)鍵字不會(huì)導(dǎo)致阻塞,但是使用鎖會(huì)導(dǎo)致阻塞