1. 程式人生 > 實用技巧 >先行發生原則(Happens-Before)

先行發生原則(Happens-Before)

本部落格系列是學習併發程式設計過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

併發程式設計系列部落格傳送門


本文是《深入Java虛擬機器》的部分讀書筆記

如果Java記憶體模型中所有的有序性都僅靠volatile和synchronized來完成,那麼有很多操作都將會變得非常囉嗦。

但是我們在編寫Java併發程式碼的時候並沒有察覺到這一點,這是因為Java語言中有一個“先行發生”(Happens-Before)的原則。這個原則非常重要,它是判斷資料是否存在競爭,執行緒是否安全的非常有用的手段。依賴這個原則,我們可以通過幾條簡單規則一攬子解決併發環境下兩個操作之間是否可能存在衝突的所有問題,而不需要陷入Java記憶體模型苦澀難懂的定義之中。

先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係,比如說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等

下面是Java記憶體模型下一些“天然的”先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推匯出來,則它們就沒有順序性保障,虛擬機器可以對它們隨意地進行重排序。

  • 程式次序規則(Program Order Rule):在一個執行緒內,按照控制流順序,書寫在前面的操作先行發生於書寫在後面的操作。注意,這裡說的是控制流順序而不是程式程式碼順序,因為要考慮分支、迴圈等結構。
  • 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裡必須強調的是“同一個鎖”,而“後面”是指時間上的先後。
  • volatile變數規則(Volatile Variable Rule):對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作,這裡的“後面”同樣是指時間上的先後。
  • 執行緒啟動規則(Thread Start Rule):Thread物件的start()方法先行發生於此執行緒的每一個動作。
  • 執行緒終止規則(Thread Termination Rule):執行緒中的所有操作都先行發生於對此執行緒的終止檢測,我們可以通過Thread::join()方法是否結束、Thread::isAlive()的返回值等手段檢測執行緒是否已經終止執行。
  • 執行緒中斷規則(Thread Interruption Rule):對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過Thread::interrupted()方法檢測到是否有中斷髮生。
  • 物件終結規則(Finalizer Rule):一個物件的初始化完成(建構函式執行結束)先行發生於它的finalize()方法的開始。
  • 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

Java語言無須任何同步手段保障就能成立的先行發生規則有且只有上面這些,下面演示一下如何使用這些規則去判定操作間是否具備順序性,對於讀寫共享變數的操作來說,就是執行緒是否安全。讀者還可以從下面這個例子中感受一下“時間上的先後順序”與“先行發生”之間有什麼不同。

private int value = 0;

pubilc void setValue(int value){
    this.value = value;
}

public int getValue(){
    return value;
}

上面的程式碼顯示的是一組再普通不過的getter/setter方法,假設存線上程A和B,執行緒A先(時間上的先後)呼叫了setValue(1),然後執行緒B呼叫了同一個物件的getValue(),那麼執行緒B收到的返回值是什麼?

我們依次分析一下先行發生原則中的各項規則。由於兩個方法分別由執行緒A和B呼叫,不在一個執行緒中,所以程式次序規則在這裡不適用;由於沒有同步塊,自然就不會發生lock和unlock操作,所以管程鎖定規則不適用;由於value變數沒有被volatile關鍵字修飾,所以volatile變數規則不適用;後面的執行緒啟動、終止、中斷規則和物件終結規則也和這裡完全沒有關係。因為沒有一個適用的先行發生規則,所以最後一條傳遞性也無從談起,因此我們可以判定,儘管執行緒A在操作時間上先於執行緒B,但是無法確定執行緒B中getValue()方法的返回結果,換句話說,這裡面的操作不是執行緒安全的。

我們至少有兩種比較簡單的方案可以選擇:要麼把getter/setter方法都定義為synchronized方法,這樣就可以套用管程鎖定規則;要麼把value定義為volatile變數,由於setter方法對value的修改不依賴value的原值,滿足volatile關鍵字使用場景,這樣就可以套用volatile變數規則來實現先行發生關係。

通過上面的例子,我們可以得出結論:一個操作“時間上的先發生”不代表這個操作會是“先行發生”。那如果一個操作“先行發生”,是否就能推匯出這個操作必定是“時間上的先發生”呢?很遺憾,這個推論也是不成立的。

上面兩個例子綜合起來證明了一個結論:時間先後順序與先行發生原則之間基本沒有因果關係,所以我們衡量併發安全問題的時候不要受時間順序的干擾,一切必須以先行發生原則為準