在討論volatile之前我們先來了解下cpu與內(nèi)存之間的關(guān)系:
手殘黨勋桶、圖丑散罕、大家心中有個(gè)大概就行了挑随。
圖中的緩存為cpu緩存状您,實(shí)際上電腦一般設(shè)有三級(jí)緩存。cpu緩存為于cpu和內(nèi)存之間的臨時(shí)存儲(chǔ)器兜挨,它的容量很小但交換速度卻比內(nèi)存快得多膏孟。
緩存的出現(xiàn)主要是為了解決CPU運(yùn)算速度與內(nèi)存讀寫速度不匹配的矛盾,因?yàn)镃PU運(yùn)算速度要比內(nèi)存讀寫速度快很多拌汇,這樣會(huì)使CPU花費(fèi)很長(zhǎng)時(shí)間等待數(shù)據(jù)到來或把數(shù)據(jù)寫入內(nèi)存柒桑。具體大家可自行百度。
volatile提供的三個(gè)特性:
-
原子性:
一個(gè)很好的例子是:32位機(jī)上的long類型讀寫操作是分為高低位讀寫兩次的担猛,并且不原子性幕垦。所謂原子性是指一個(gè)集合操作中所有操作“同生共死”丢氢、要么一起成功執(zhí)行要么一起不執(zhí)行傅联。
long i = 0; i = 10;
當(dāng)有一條線程執(zhí)行到
i = 10
時(shí),首先會(huì)為低16位進(jìn)行賦值疚察,倘若此時(shí)有另一條線程來讀取時(shí)只會(huì)讀到只賦值的低16位的數(shù)據(jù)蒸走,從而造成bug的出現(xiàn)。
不過volatile并不能代替鎖貌嫡,它無(wú)法保證復(fù)合操作的原子性比驻,例如:i++
,實(shí)際上此操作是由三個(gè)步驟組成的:首先取得i的值该溯,再對(duì)i進(jìn)行+1,再將結(jié)果寫回i别惦。 -
可見性:
就如上圖所示狈茉,每個(gè)CPU都有屬于自己的緩存
int i = 0; 線程1執(zhí)行的代碼 i = 10; 線程2執(zhí)行的代碼 j = i;
假設(shè)線程1先于線程2執(zhí)行,并且CPU1執(zhí)行線程1掸掸、CPU2執(zhí)行線程2氯庆。
當(dāng)線程1執(zhí)行 i =10這句時(shí),會(huì)先把i的初始值加載到CPU1的高速緩存中扰付,然后賦值為10堤撵,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到內(nèi)存當(dāng)中羽莺。
此時(shí)線程2執(zhí)行 j = i实昨,它會(huì)先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i
的值還是0盐固,那么就會(huì)使得j的值為0荒给,而不是10。
這就是可見性問題刁卜,線程1對(duì)變量i修改了之后锐墙,線程2沒有立即看到線程1修改的值。
當(dāng)一個(gè)共享變量被volatile修飾時(shí)长酗,它會(huì)保證修改的值會(huì)立即被更新到內(nèi)存中溪北,當(dāng)有其他線程需
要讀取時(shí),它會(huì)去內(nèi)存中讀取新值夺脾。
-
有序性:
大部分人會(huì)認(rèn)為程序執(zhí)行順序會(huì)和代碼編寫順序一樣之拨,其實(shí)不然。在JMM內(nèi)存模型中允許編譯器和處理器對(duì)指令進(jìn)行重新排序來進(jìn)行優(yōu)化咧叭、以便于CPU能夠并行執(zhí)行指令蚀乔,從而提高效率。
在單線程中重排序的結(jié)果不會(huì)影響程序的運(yùn)行結(jié)果菲茬,但卻會(huì)影響多線程并行執(zhí)行的正確性了吉挣。
舉個(gè)簡(jiǎn)單的栗子:Thread 1 Thread 2 1:r2 = A 3:r1 = b 2:b = 1 4:A = 2
從順序上看 (r2 == 2) 、(r1 == 1) 應(yīng)該不可能出現(xiàn)婉弹,但如果被重排序成下列順序是就不一定了:
Thread 1 Thread 2 b = 1 r1 = b r2 = A A = 2
再舉一個(gè)稍微復(fù)雜的例子:
class order { int a = 0; boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) { int i = a + 1; dosomething; } } }
假設(shè)線程A先執(zhí)行writer()方法睬魂,然后線程B執(zhí)行reader()方法。當(dāng)發(fā)生指令重排序后镀赌,writer()方法中的flag的寫入可能會(huì)先于a的寫入氯哮,造成線程B在執(zhí)行reader方法時(shí)判斷正確并為i賦值。
指令重排序帶來的可見性問題
雖然指令重排序會(huì)帶來許多問題商佛,但卻能有效提高效率喉钢,并且在串行代碼中大家可放心:
指令重排序可以保證串行語(yǔ)義一致姆打,但沒有義務(wù)保證多線程之間的語(yǔ)義也一致。
Java虛擬機(jī)還規(guī)定了些規(guī)則指定了哪些指令不能重排序
Happen-Before 規(guī)則:
- 程序順序原則:一個(gè)線程內(nèi)保證語(yǔ)義的串行性
- volatile規(guī)則:volatile變量的寫肠虽,先發(fā)送于讀棒假,保證volatile變量的可見性
- 鎖規(guī)則:解鎖必然先發(fā)生于隨后的加鎖前
- 傳遞性:A先于B勺爱,B先于C甲献,那么A必然先于C
- 線程的start()方法先于它的每一個(gè)動(dòng)作
- 線程的中斷先于被中斷程序的代碼
- 對(duì)象的構(gòu)造函數(shù)的執(zhí)行镀首,結(jié)束先于finalized()方法
總的來說,volatile還涉及到JMM內(nèi)存模型等相關(guān)知識(shí)伯复。推薦大家去看《Java并發(fā)編程的藝術(shù)》和《Java高并發(fā)程序設(shè)計(jì)》慨代,里面講解更加透徹。