安全併發之先行發生原則
先行發生原則,可以幫你判定是否併發安全的,從而不必去猜測是否是執行緒安全了!
如果Java記憶體模型中所有有序性都靠volatile和synchronized來完成,那麼編寫程式碼會很繁瑣,但日常Java開發中並沒有感受到這一點,正是因為Java語言的“先行發生”原則。這個原則非常重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據。
先行發生是Java記憶體模型中定義的兩項運算元之間的偏序關係,如果說操作A先行發生於操作B,就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。
下面是Java記憶體模型中一些“天然的”先行發生關係,這些先行發生關係無需任何同步協助器協作即可存在,可以直接在編碼中使用。如果兩個關係不在此列,而又無法通過這些關係推匯出來,它們的順序就無法保證,虛擬機器可以對它們任意重排序。
程式次序法則: 執行緒中的每一個動作A都happens-before於該執行緒中的每一個動作B,其中,線上程中,所有的動作B都出現在動作A之後
管程鎖定規則: 對於一個監視器鎖的unLock 操作happens-before於每個後續對同一監視器鎖的Lock操作
volatile變數法則: 對volatile域的寫入操作happens-before於每個後續對同一個yu域的讀操作。
執行緒啟動法則: 在同一個執行緒裡,對Thread.start的呼叫會happens-before於每一個啟動執行緒中的動作。
執行緒終結法則: 執行緒中的所有動作都happens-before於其它執行緒檢測到這個執行緒已經終結,或者從Thread.jonin呼叫成功返回,或者Thread.isAlive返回false.
中斷法則: 一個執行緒呼叫另一個執行緒的interrupt happens-before 於被中斷的執行緒發現中斷(通過丟擲InterruptedException 或者呼叫isInterrupted和interrupted)
終結法則: 一個物件的建構函式的結束happens-before於這個物件finalizer的開始
傳遞性: 如果 A happens-before 於 B,且 B happens-before 於 C,則 A happens-before 於C。
java無須任何手段即可保證上面的先行發生規則成立,下面那個例子看一下:
private int value = 0; public void setValue(int value){ this.value = value; } public int getValue(){ return value; }
假設A、B兩個執行緒,執行緒A先(時間上的先後)執行setValue(1), 然後執行緒B呼叫同一物件的getValue(),那麼執行緒B收到的返回值是什麼?
依次分析一下先行發生原則中的各個原則:
由於兩個方法分別在不同的執行緒中被呼叫,程式次序原則不適用;
沒有同步塊,自然不會發生lock和unlock操作,管程鎖定原則不適用;
value變數沒有被volatile修飾,volatile變數原則不適用;
後面的執行緒啟動、中斷、終止原則也毫無關係;
沒有一個適用的原則,傳遞性也不適用。
所以說執行緒B得到的結果不確定是0還是1,換句話說,這裡面的操作不是執行緒安全的。
怎麼修復呢?getter/setter 定義synchronized方法;或者把value變數定義volatile變數,就回到了先行發生原則上了。
private volatile int value = 0;
另外,先行發生並不代表一定是先發生!
時間先後順序於先行發生的原則之間基本沒有太大的關係。
比如如下程式碼中,i, j 的取值問題:
//同一個執行緒中執行 int i = 1; int j = 2; // doSth... volt = 10; // 假設volt為 volatile 修飾的
根據程式次序規則,”int i = 1”的操作先行發生於”int j = 2”,但是”int j = 2”的程式碼完全可能先被處理器執行,這並不影響先行發生原則的正確性,因為我們在這條執行緒中並沒有辦法感知到這點。
先行發生原則,可以幫你判定是否併發安全的,從而不必去猜測是否是執行緒安全了!
---- 摘自《深入理解Java虛擬機器》