一苟弛、volatile的作用詳解
1备典、防重排序
? ? ? ? 在并發(fā)環(huán)境下的單例實現方式诫欠,我們通成恚可以采用雙重檢查加鎖(DCL)的方式來實現莱褒,需要在變量singleton之間加上volatile關鍵字。因為操作系統可以對指令進行重排序涎劈,在多線程環(huán)境下就可能將一個未初始化的對象引用暴露出來广凸,從而導致不可預料的結果。因此蛛枚,為了防止這個過程的重排序谅海,我們需要將變量設置為volatile類型的變量。
2蹦浦、實現可見性
? ? ? ?可見性問題主要指一個線程修改了共享變量值扭吁,而另一個線程卻看不到。引起可見性問題的主要原因是每個線程擁有自己的一個高速緩存區(qū)—線程工作內存白筹,使用volatile關鍵字會把數據副本更新到主存中智末,另外一個線程也可獲取到最新的數據谅摄。
3徒河、保證原子性:單次讀/寫
? ? ? ? 基于volatile保證單次的讀/寫操作具有原子性的理解,
問題1: i++為什么不能保證原子性?
? ? ? ?對于原子性送漠,需要強調一點顽照,也是大家容易誤解的一點:對volatile變量的單次讀/寫操作可以保證原子性的,如long和double類型變量闽寡。但是并不能保證i++這種操作的原子性代兵,因為本質上i++是讀、寫兩次操作爷狈。
i++其實是一個復合操作植影,包括三步驟:
1)讀取i的值。
2)對i加1涎永。
3)將i的值寫回內存思币。
? ? ? ? volatile是無法保證這三個操作是具有原子性的,我們可以通過AtomicInteger或者Synchronized來保證+1操作的原子性羡微。
問題2: 共享的long和double變量的為什么要用volatile?
? ? ? ? 因為long和double兩種數據類型的操作可分為高32位和低32位兩部分谷饿,因此普通的long或double類型讀/寫可能不是原子的。因此妈倔,鼓勵大家將共享的long和double變量設置為volatile類型博投,這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性。
? ? ? 目前各種平臺下的商用虛擬機都選擇把 64 位數據的讀寫操作作為原子操作來對待盯蝴,因此我們在編寫代碼時一般不把long 和 double 變量專門聲明為 volatile,多數情況下也是不會錯的毅哗。
二听怕、volatile 的實現原理
(一)volatile 可見性實現
? ? ? ??volatile 變量的內存可見性是基于內存屏障(Memory Barrier)實現:
? ? ? ?內存屏障,又稱內存柵欄虑绵,是一個 CPU 指令叉跛。在程序運行時,為了提高執(zhí)行性能蒸殿,編譯器和處理器會對指令進行重排序筷厘,JMM 為了保證在不同的編譯器和 CPU 上有相同的結果,通過插入特定類型的內存屏障來禁止+ 特定類型的編譯器重排序和處理器重排序宏所。插入一條內存屏障會告訴編譯器和 CPU:不管什么指令都不能和這條 Memory Barrier 指令重排序酥艳。如果對聲明了 volatile 的變量進行寫操作,JVM 就會向處理器發(fā)送一條 lock 前綴的指令爬骤,將這個變量所在緩存行的數據寫回到系統內存充石。
? ? ? ?為了保證各個處理器的緩存是一致的,實現了緩存一致性協議(MESI)霞玄。每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了骤铃,當處理器發(fā)現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態(tài)坷剧,然后重新從系統內存中把數據讀到處理器緩存里惰爬。所有多核處理器下還會完成:當處理器發(fā)現本地緩存失效后,就會從內存中重讀該變量數據惫企,即可以獲取當前最新值撕瞧。volatile 變量通過這樣的機制就使得每個線程都能獲得該變量的最新值。
1)lock 指令
? ? ? ?在 Pentium 和早期的 IA-32 處理器中狞尔,lock 前綴會使處理器執(zhí)行當前指令時產生一個 LOCK# 信號丛版,會對總線進行鎖定,其它 CPU 對內存的讀寫請求都會被阻塞偏序,直到鎖釋放页畦。后來的處理器,加鎖操作是由高速緩存鎖代替總線鎖來處理研儒。因為鎖總線的開銷比較大豫缨。這種場景多緩存的數據一致通過緩存一致性協議(MESI)來保證。
2)緩存一致性
? ? ? ?緩存是分段(line)的殉摔,一個段對應一塊存儲空間州胳,稱之為緩存行,它是 CPU 緩存中可分配的最小存儲單元逸月,大小 32 字節(jié)栓撞、64 字節(jié)、128 字節(jié)不等,這與 CPU 架構有關瓤湘,通常來說是 64 字節(jié)瓢颅。
? ? ? ?LOCK# 因為鎖總線效率太低,因此使用了多組緩存弛说。為了使其行為看起來如同一組緩存那樣挽懦。因而設計了緩存一致性協議。緩存一致性協議有多種木人,但是日常處理的大多數計算機設備都屬于 " 嗅探(snooping)" 協議信柿。
? ? ? ?所有內存的傳輸都發(fā)生在一條共享的總線上,而所有的處理器都能看到這條總線醒第。緩存本身是獨立的渔嚷,但是內存是共享資源,所有的內存訪問都要經過仲裁(同一個指令周期中稠曼,只有一個 CPU 緩存可以讀寫內存)形病。
? ? ? ? CPU 緩存不僅僅在做內存?zhèn)鬏數臅r候才與總線打交道,而且還不停在嗅探總線上發(fā)生的數據交換霞幅,跟蹤其他緩存在做什么漠吻。?當一個緩存代表它所屬的處理器去讀寫內存時,其它處理器都會得到通知司恳,它們以此來使自己的緩存保持同步途乃。只要某個處理器寫內存,其它處理器馬上知道這塊內存在它們的緩存段中已經失效抵赢。
(二)volatile 有序性實現
volatile 的 happens-before 關系
? ? ? ?happens-before 規(guī)則中有一條是 volatile 變量規(guī)則:對一個 volatile 域的寫欺劳,happens-before 于任意后續(xù)對這個 volatile 域的讀。
volatile 禁止重排序
? ? ? ? 為了性能優(yōu)化铅鲤,JMM 在不改變正確語義的前提下,會允許編譯器和處理器對指令序列進行重排序枫弟。JMM 提供了內存屏障阻止這種重排序邢享。而volatile就是屏蔽指令的代碼實現。
? ? ? ?" NO " 表示禁止重排序淡诗。為了實現 volatile 內存語義時骇塘,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序韩容。對于編譯器來說款违,發(fā)現一個最優(yōu)布置來最小化插入屏障的總數幾乎是不可能的,為此群凶,JMM 采取了保守的策略插爹。
在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。
在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障。
在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障赠尾。
在每個 volatile 讀操作的后面插入一個 LoadStore 屏障力穗。
? ? ? ?volatile 寫是在前面和后面分別插入內存屏障,而 volatile 讀操作是在后面插入兩個內存屏障气嫁。
內存屏障說明
StoreStore 屏障 禁止上面的普通寫和下面的 volatile 寫重排序当窗。
StoreLoad 屏障 防止上面的 volatile 寫與下面可能有的 volatile 讀/寫重排序。
LoadLoad 屏障 禁止下面所有的普通讀操作和上面的 volatile 讀重排序寸宵。
LoadStore 屏障 禁止下面所有的普通寫操作和上面的 volatile 讀重排序崖面。
三、volatile 的應用場景
使用 volatile 必須具備的條件:
1梯影、對變量的寫操作不依賴于當前值嘶朱。
2、該變量沒有包含在具有其他變量的不變式中光酣。
3疏遏、只有在狀態(tài)真正獨立于程序內其他內容時才能使用 volatile。
模式1:狀態(tài)標志
? ? ? ? 實現 volatile 變量的規(guī)范使用僅僅是使用一個布爾狀態(tài)標志救军。用于指示發(fā)生了一個重要的一次性事件财异,例如完成初始化或請求停機。
模式2:一次性安全發(fā)布(one-time safe publication)
? ? ? ? 缺乏同步會導致無法實現可見性
模式3:獨立觀察(independent observation)
? ? ? ?安全使用 volatile 的另一種簡單模式是定期 發(fā)布 觀察結果供程序內部使用唱遭。例如戳寸,假設有一種環(huán)境傳感器能夠感覺環(huán)境溫度。一個后臺線程可能會每隔幾秒讀取一次該傳感器拷泽,并更新包含當前文檔的 volatile 變量疫鹊。然后,其他線程可以讀取這個變量司致,從而隨時能夠看到最新的溫度值拆吆。
模式4:volatile bean 模式
? ? ? ?在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的脂矫,并且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外枣耀,不能包含任何邏輯。此外庭再,對于對象引用的數據成員捞奕,引用的對象必須是有效不可變的。(這將禁止具有數組值的屬性拄轻,因為當數組引用被聲明為 volatile 時颅围,只有引用而不是數組本身具有 volatile 語義)。對于任何 volatile 變量恨搓,不變式或約束都不能包含 JavaBean 屬性院促。
模式5:開銷較低的讀-寫鎖策略
? ? ? ?volatile 的功能還不足以實現計數器。如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷一疯。安全的計數器使用 synchronized 確保增量操作是原子的撼玄,并使用 volatile 保證當前結果的可見性。如果更新不頻繁的話墩邀,該方法可實現更好的性能掌猛,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優(yōu)于一個無競爭的鎖獲取的開銷眉睹。
模式6:雙重檢查(double-checked)
? ? ? ?就是我們上文舉的例子荔茬。單例模式的一種實現方式,但很多人會忽略 volatile 關鍵字竹海,因為沒有該關鍵字慕蔚,程序也可以很好的運行,只不過代碼的穩(wěn)定性總不是 100%斋配,說不定在未來的某個時刻孔飒,隱藏的 bug 就出來了。