先行發生原則(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變量規則來實現先行發生關係。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章