java中的volatile有兩個(gè)語義:
- 保證共享變量可見性
通俗來說就是蔫缸,某個(gè)線程對(duì)一個(gè)volatile變量的修改,對(duì)于其它線程來說是可見的际起,即線程每次獲取volatile變量的值都是最新的拾碌。
- 保證共享變量可見性
- 禁止指令重排序
1.java內(nèi)存模型
- 線程之間的共享變量存儲(chǔ)在主內(nèi)存中,所謂的共享變量街望,是指所有存儲(chǔ)在堆內(nèi)存上的實(shí)例域校翔、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中。
- 每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory)灾前,本地內(nèi)存中存儲(chǔ)了該線程讀/寫共享變量的副本防症。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在哎甲。它涵蓋了緩存蔫敲,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化炭玫。
2.重排序
在執(zhí)行程序時(shí)為了提高性能奈嘿,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序有以下三種類型:
- 編譯器優(yōu)化的重排序吞加。編譯器在不改變單線程程序語義的前提下裙犹,可以重新安排語句的執(zhí)行順序酝惧。下面代碼中a、b的賦值順序伯诬,被編譯之后可能就變成了先設(shè)置b晚唇,再設(shè)置a。
public void set() {
a = 1;
b = 1;
}
- 指令級(jí)并行的重排序〉了疲現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism哩陕, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性赫舒,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序悍及。
-
內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)接癌,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行心赶。
1、CPU執(zhí)行l(wèi)oad讀數(shù)據(jù)時(shí)缺猛,把讀請(qǐng)求放到LoadBuffer缨叫,這樣就不用等待其它CPU響應(yīng),先進(jìn)行下面操作荔燎,稍后再處理這個(gè)讀請(qǐng)求的結(jié)果耻姥。
2、CPU執(zhí)行store寫數(shù)據(jù)時(shí)有咨,把數(shù)據(jù)寫到StoreBuffer中琐簇,待到某個(gè)適合的時(shí)間點(diǎn),把StoreBuffer的數(shù)據(jù)刷到主存中座享。
由于StoreBuffer和LoadBuffer是異步執(zhí)行的婉商,所以在外面看來,先寫后讀渣叛,還是先讀后寫丈秩,沒有嚴(yán)格的固定順序。
從java源代碼到最終實(shí)際執(zhí)行的指令序列诗箍,會(huì)分別經(jīng)歷下面三種重排序:
上述的1屬于編譯器重排序癣籽,2和3屬于處理器重排序挽唉。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題滤祖。
下面舉例說明處理器重排序帶來的可見性問題
假設(shè)處理器A和處理器B按程序的順序并行執(zhí)行內(nèi)存訪問,最終卻可能得到x = y = 0的結(jié)果瓶籽。具體的原因如下圖所示:
這里處理器A和處理器B可以同時(shí)把共享變量寫入自己的寫緩沖區(qū)(A1匠童,B1),然后從內(nèi)存中讀取另一個(gè)共享變量(A2塑顺,B2)汤求,最后才把自己寫緩存區(qū)中保存的臟數(shù)據(jù)刷新到內(nèi)存中(A3俏险,B3)。當(dāng)以這種時(shí)序執(zhí)行時(shí)扬绪,程序就可以得到x = y = 0的結(jié)果竖独。
從內(nèi)存操作實(shí)際發(fā)生的順序來看,直到處理器A執(zhí)行A3來刷新自己的寫緩存區(qū)挤牛,寫操作A1才算真正執(zhí)行了莹痢。雖然處理器A執(zhí)行內(nèi)存操作的順序?yàn)椋篈1->A2,但內(nèi)存操作實(shí)際發(fā)生的順序卻是:A2->A1墓赴。此時(shí)竞膳,處理器A的內(nèi)存操作順序被重排序了(處理器B的情況和處理器A一樣,這里就不贅述了)诫硕。
這里的關(guān)鍵是坦辟,由于寫緩沖區(qū)僅對(duì)自己的處理器可見,它會(huì)導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會(huì)與內(nèi)存實(shí)際的操作執(zhí)行順序不一致章办。由于現(xiàn)代的處理器都會(huì)使用寫緩沖區(qū)锉走,因此現(xiàn)代的處理器都會(huì)允許對(duì)寫-讀操做重排序。
那么JMM是如何禁止重排序從而保證可見性的呢藕届?
- 對(duì)于編譯器挠日,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
- 對(duì)于處理器重排序翰舌,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí)嚣潜,插入特定類型的內(nèi)存屏障(memory barriers,intel稱之為memory fence)指令椅贱,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)懂算。
3.內(nèi)存屏障
跟據(jù)上面的描述,為了保證內(nèi)存可見性庇麦,java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序计技。內(nèi)存屏障是一組處理指令,用來實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制山橄。JMM把內(nèi)存屏障指令分為下列四類:
4. happens-before
從JDK5開始垮媒,java使用新的JSR -133內(nèi)存模型(本文除非特別說明,針對(duì)的都是JSR- 133內(nèi)存模型)航棱。JSR-133提出了happens-before的概念睡雇,通過這個(gè)概念來闡述操作之間的內(nèi)存可見性。如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見饮醇,那么這兩個(gè)操作之間必須存在happens-before關(guān)系它抱。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間朴艰。與程序員密切相關(guān)的happens-before規(guī)則如下:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作观蓄,happens- before 于該線程中的任意后續(xù)操作混移。
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖侮穿。
- volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫歌径,happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。
- 傳遞性:如果A happens- before B亲茅,且B happens- before C沮脖,那么A happens- before C。
happens-before與JMM的關(guān)系如下圖所示:
如上圖所示芯急,一個(gè)happens-before規(guī)則通常對(duì)應(yīng)于多個(gè)編譯器重排序規(guī)則和處理器重排序規(guī)則勺届。對(duì)于java程序員來說,happens-before規(guī)則簡單易懂娶耍,它避免程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)免姿。
5.volatile的禁止重排序與保證可見性
- 編譯器對(duì)操作volatile變量的代碼不再進(jìn)行優(yōu)化
- volatile底層通過內(nèi)存屏障實(shí)現(xiàn)禁止特定類型的處理器重排序
為了實(shí)施volatile對(duì)應(yīng)的happens-before規(guī)則蘑险,對(duì)volatile變量的內(nèi)存操作涉及到的內(nèi)存屏障如下:
以volatile store -> volatile load為例讼庇,通過插入StroreLoad內(nèi)存屏障荷荤,確保Store數(shù)據(jù)對(duì)其他處理器變得可見(指刷新到內(nèi)存)单山,之前于Load2及所有后續(xù)裝載指令的裝載。從而實(shí)現(xiàn)黔牵,某個(gè)線程對(duì)volatile變量的修改會(huì)立即刷新到主內(nèi)存米者,并導(dǎo)致其它線程工作內(nèi)存中的副本無效德召,讀取時(shí)只能從主內(nèi)存加載最新的值辑舷。
參考鏈接:
1.http://www.infoq.com/cn/articles/java-memory-model-1
2.http://www.importnew.com/23520.html