Java內存模型告訴咱們,各個線程會將共享變量從主內存中拷貝到工做內存,而後執行引擎會基於工做內存中的數據進行操做處理。 線程在工做內存進行操做後什麼時候會寫到主內存中? 這個時機對普通變量是沒有規定的,而針對volatile修飾的變量給Java 虛擬機特殊的約定,線程對 volatile變量的修改會馬上被其餘線程所感知,即不會出現數據髒讀的現象,從而保證數據的「可見性」。緩存
一言以蔽之,被volatile修飾的變量可以保證每一個線程可以獲取該變量的最新值,從而避免出現數據髒讀的現象。性能優化
volatile是怎樣實現了?好比一個很簡單的Java代碼:bash
instance = new Instancce() //instance是volatile變量複製代碼
在生成彙編代碼時會在volatile修飾的共享變量進行寫操做的時候會多出Lock前綴的指令。 咱們想這個Lock指令確定有神奇的地方,那麼Lock前綴的指令在多核處理器下會發現什麼事情了?主要有這兩個方面的影響:app
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。 若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。性能
在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。 所以,通過分析咱們能夠得出以下結論:優化
這樣volatile變量經過這樣的機制就使得每一個線程都能得到該變量的最新值。spa
happens-before中的volatile 變量規則(Volatile Variable Rule):對一個 volatile 變量的寫操做先行發生於後面對這個變量的讀操做。線程
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}複製代碼
對應的happens-before關係以下:code
加鎖線程A先執行writer方法,而後線程B執行reader方法。 圖中每個箭頭兩個節點就代碼一個happens-before關係:cdn
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}複製代碼
假設線程A先執行writer方法,線程B隨後執行reader方法,初始時線程的本地內存中flag和a都是初始狀態,下圖是線程A執行volatile寫後的狀態圖:
當volatile變量寫後,線程B中本地內存中共享變量就會置爲失效的狀態,所以線程B須要從主內存中去讀取該變量的最新值。下圖就展現了線程B讀取同一個volatile變量的內存變化示意圖:
從橫向來看,線程A和線程B之間進行了一次通訊,線程A在寫volatile變量時,實際上就像是給B發送了一個消息告訴線程B你如今的值都是舊的了,而後線程B讀這個volatile變量時就像是接收了線程A剛剛發送的消息。既然是舊的了,那線程B該怎麼辦了?天然而然就只能去主內存去取啦。
爲了性能優化,JMM在不改變正確語義的前提下,會容許編譯器和處理器對指令序列進行重排序,那若是想阻止重排序要怎麼辦了? 答案是能夠添加內存屏障。
四類JMM內存屏障:
Java編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。 爲了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:
"NO"表示禁止重排序。 爲了實現volatile內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。 對於編譯器來講,發現一個最優佈置來最小化插入屏障的總數幾乎是不可能的,爲此,JMM採起了保守策略:
須要注意的是:volatile寫操做是在前面和後面分別插入內存屏障,而volatile讀操做是在後面插入兩個內存屏障。