1. 程式人生 > >java內存模型—先行發生原則

java內存模型—先行發生原則

來看 深入 安全問題 rup 語句 修改 構造函數 serial 參考資料

Java語言中有一個“先行發生”(happens-before)的原則。這個原則非常重要,它是判斷數據是否存在競爭,線程是否安全的主要依據,依賴這個原則,我們可以通過幾條規則一攬子解決並發環境下兩個操作之間是否可能存在沖突的所有問題。
現在就來看看“先行發生”原則指的是什麽。先行發生是Java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發送了消息、調用了方法等。這句話不難理解,但它意味著什麽呢?我們可以舉個例子來說明一下,如下所示的這三句偽代碼:

        //以下操作在線程A中執行
        i=1;
        //以下操作在線程B中執行
        j=i;
        //以下操作在線程C中執行
        i=2;

假設線程A中的操作“i=1”先行發生於線程B的操作“j=i”,那我們就可以確定在線程B的操作執行後,變量j的值一定是等於1,得出這個結論的依據有兩個,一是根據先行發生原則,“i=1”的結果可以被觀察到;二是線程C登場之前,線程A操作結束之後沒有其他線程會修改變量i的值。現在再來考慮線程C,我們依然保持線程A和B之間的先行發生關系,而C出現在線程A和B的操作之間,但是C與B沒有先行發生關系,那j的值會是多少呢?答案是不確定!1和2都有可能,因為線程C對變量i的影響可能會被線程B觀察到,也可能不會,這時候線程B就存在讀取到過期數據的風險,不具備多線程安全性。

八種“天然的”先行發生關系

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

  • 程序次序規則(ProgramOrderRule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程序代碼順序,因為要考慮分支、循環等結構。該happens-before關系本質上和as-if-serial語義是一回事:
    • as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關系保證正確同步的多線程程序的執行結果不被改變。
    • as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關系給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。as-if-serial語義和happens-before這麽做的目的,都是為了在不改變程序執行結果的前提下,盡可能地提高程序執行的並行度。

  • 管程鎖定規則(MonitorLockRule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是同一個鎖,而“後面”是指時間上的先後順序。
  • volatile變量規則(VolatileVariableRule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”同樣是指時間上的先後順序。
  • 線程啟動規則(ThreadStartRule):Thread對象的start()方法先行發生於此線程的每一個動作。
  • 線程終止規則(ThreadTerminationRule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
  • 線程中斷規則(ThreadInterruptionRule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷發生。
  • 對象終結規則(FinalizerRule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
  • 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

舉個例子

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

    private int value = 0;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = 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變量規則來實現先行發生關系。
通過上面的例子,我們可以得出結論:一個操作“時間上的先發生”不代表這個操作會是“先行發生”,那如果一個操作“先行發生”是否就能推導出這個操作必定是“時間上的先發生”呢?很遺憾,這個推論也是不成立的,一個典型的例子就是多次提到的“指令重排序”。

//以下操作在同一個線程中執行
inti=1;
intj=2;

以上代碼的兩條賦值語句在同一個線程之中,根據程序次序規則,“inti=1”的操作先行發生於“intj=2”,但是“intj=2”的代碼完全可能先被處理器執行,這並不影響先行發生原則的正確性,因為我們在這條線程之中沒有辦法感知到這點。

總結

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

參考資料

周誌明:《深入理解Java虛擬機》

方騰飛:《Java並發編程的藝術》

java內存模型—先行發生原則