很重要的 搞清楚happen-before -->Java併發編程之happens-before 感謝 Java併發編程:volatile關鍵字解析 可見性:一個線程對主記憶體的修改可以及時的被其他線程觀察到。 有序性:一個線程觀察其他線程中的指令執行順序,由於指令 重排序的存在,該觀察結果一般雜亂無 ...
很重要的 搞清楚happen-before -->Java併發編程之happens-before
- 可見性:一個線程對主記憶體的修改可以及時的被其他線程觀察到。
- 有序性:一個線程觀察其他線程中的指令執行順序,由於指令 重排序的存在,該觀察結果一般雜亂無序。
- 原子性:提供了互斥訪問。
特性 | 操作 |
---|---|
可見性 | 可以由final(不會修改)、volatile(強制更新+讀取主記憶體)以及synchronized(在unlock時會刷新所有已修改數據到主記憶體,lock時會從主記憶體重新載入數據)實現 |
有序性 | 可以由volatile(禁止指令重排序)/synchronized(一個變數最多只能有一個線程對其lock)實現 |
原子性 | 只有synchronized可以實現原子性,保證synchronized的代碼塊串列執行 |
synchronized在加鎖時進行數據的重載入(主記憶體 -> 工作記憶體),在釋放鎖時將數據刷新到主記憶體。
衝突訪問(Conflicting Accesses) 對同一個共用欄位或數組元素存在兩個訪問(讀或寫),且至少有一個訪問是寫操作,就稱作有衝突。當程式包含兩個沒有被 happens-before 關係排序的衝突訪問時,就稱存在數據爭用。
動作A對於動作B是happen-before,即意味著動作A的修改(所有數據)對於動作B是可見的。
happens-before關係如下:
- 某個線程中的每個動作都 happens-before 該線程中該動作後面的動作(這裡無論指令是否重排序都必須滿足)。
- 某個管程 m 上的解鎖動作 synchronizes-with 所有後續在 m 上的鎖定動作(這裡的後續是根據同步順序定義的)。
- 對 volatile 變數 v 的寫操作 synchronizes-with 所有後續任意線程對 v 的讀操作(這裡的後續是根據同步順序定義的)。
- 用於啟動一個線程的動作(start方法) synchronizes-with 該新啟動線程中的第一個動作。
- 初始化動作對於其他線程的第一個調用是happen-before
- happen-before是閉包傳遞的。
- 個程式不存在數據爭用,那麼該程式是順序一致的,即該程式不存在可見性問題。但是該程式還是可能在原子訪問上出現問題,所以不存在數據爭用並不是意味著線程安全,只有加上原子性保證才是線程安全的。這也是為什麼synchronized是線程安全的,而volatile(保證所有變數訪問都是happen-before)只有在所有操作都是原子的情況下才是線程安全的原因。
另外,Java記憶體模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下麵就來具體介紹下happens-before原則(先行發生原則):
- 程式次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
- 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
- volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
- 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
- 線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
- 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
- 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
- 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
這8條原則摘自《深入理解Java虛擬機》。
這8條規則中,前4條規則是比較重要的,後4條規則都是顯而易見的。
下麵我們來解釋一下前4條規則:
對於程式次序規則來說,我的理解就是一段程式代碼的執行在單個線程中看起來是有序的。註意,雖然這條規則中提到“書寫在前面的操作先行發生於書寫在後面的操作”,這個應該是程式看起來執行的順序是按照代碼順序執行的,因為虛擬機可能會對程式代碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程式順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此,在單個線程中,程式執行看起來是有序執行的,這一點要註意理解。事實上,這個規則是用來保證程式在單線程中執行結果的正確性,但無法保證程式在多線程中執行的正確性。
第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出於被鎖定的狀態,那麼必須先對鎖進行了釋放操作,後面才能繼續進行lock操作。
第三條規則是一條比較重要的規則,也是後文將要重點講述的內容。直觀地解釋就是,如果一個線程先去寫一個變數,然後一個線程去進行讀取,那麼寫入操作肯定會先行發生於讀操作。
第四條規則實際上就是體現happens-before原則具備傳遞性。
深入volatile的
1.volatile關鍵字的兩層語義
一旦一個共用變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了不同線程對這個變數進行操作時的可見性,即一個線程修改了某個變數的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
2.volatile的原理和實現機制
前面講述了源於volatile關鍵字的一些使用,下麵我們來探討一下volatile到底如何保證可見性和禁止指令重排序的。
下麵這段話摘自《深入理解Java虛擬機》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock首碼指令”
lock首碼指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。
使用場景
synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程式執行效率,而volatile關鍵字在某些情況下性能要優於synchronized,但是要註意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:
1)對變數的寫操作不依賴於當前值
2)該變數沒有包含在具有其他變數的不變式中
實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立於任何程式的狀態,包括變數的當前狀態。
事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程式在併發時能夠正確執行。
下麵列舉幾個Java中使用volatile的幾個場景。
1.狀態標記量
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
volatile
boolean
inited =
false
;
//線程1:
context = loadContext();
inited =
true
;
//線程2:
while
(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程式執行效率,而volatile關鍵字在某些情況下性能要優於synchronized,但是要註意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:
1)對變數的寫操作不依賴於當前值
2)該變數沒有包含在具有其他變數的不變式中
實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立於任何程式的狀態,包括變數的當前狀態。
事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程式在併發時能夠正確執行。
下麵列舉幾個Java中使用volatile的幾個場景。
1.狀態標記量
1 2 3 4 5 6 7 8 9 |
volatile boolean flag = false ;
while (!flag){
doSomething();
}
public void setFlag() {
flag = true ;
}
|