java之happens-before

如果Java內(nèi)存模型中所有的有序性都僅僅靠volatile和synchronized來完成,那么有一些操作將會變得很煩瑣,但是我們在編寫Java并發(fā)代碼的時候并沒有感覺到這一點,這是因為Java語言中有一個“先行發(fā)生”(happens-before)的原則。 這個原則非常重要,它是判斷數(shù)據(jù)是否存在競爭、 線程是否安全的主要依據(jù),依靠這個原則,我們可以通過幾條規(guī)則一攬子地解決并發(fā)環(huán)境下兩個操作之間是否可能存在沖突的所有問題。

一、什么是先行發(fā)生原則
現(xiàn)在就來看看“先行發(fā)生”原則指的是什么。 先行發(fā)生是Java內(nèi)存模型中定義的兩項操作之間的偏序關(guān)系,如果說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到。“影響”包括修改了內(nèi)存中共享變量的值、 發(fā)送了消息、 調(diào)用了方法等。 這句話不難理解,但它意味著什么呢?我們可以舉個例子來說明一下,如下偽代碼:

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

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

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

  1. 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。 準確地說,應該是控制流順序而不是程序代碼順序,因為要考慮分支、 循環(huán)等結(jié)構(gòu)。
  2. 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。 這里必須強調(diào)的是同一個鎖,而“后面”是指時間上的先后順序。
  3. volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后順序。
  4. 線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
  5. 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、 Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
  6. 線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
  7. 對象終結(jié)規(guī)則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
  8. 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

三、如何應用先行發(fā)生規(guī)則
Java語言無須任何同步手段保障就能成立的先行發(fā)生規(guī)則就只有上面這些了,筆者演示一下如何使用這些規(guī)則去判定操作間是否具備順序性,對于讀寫共享變量的操作來說,就是線程是否安全,讀者還可以從下面這個例子中感受一下“時間上的先后順序”與“先行發(fā)生”之間有什么不同:

private int value=0;
pubilc void setValue(int value){
    this.value=value;
}
public int getValue(){
    return value;
}

以上顯示的是一組再普通不過的getter/setter方法,假設(shè)存在線程A和B,線程A先(時間上的先后)調(diào)用了“setValue(1)”,然后線程B調(diào)用了同一個對象的“getValue()”,那么線程B收到的返回值是什么?

我們依次分析一下先行發(fā)生原則中的各項規(guī)則,由于兩個方法分別由線程A和線程B調(diào)用,不在一個線程中,所以程序次序規(guī)則在這里不適用;由于沒有同步塊,自然就不會發(fā)生lock和unlock操作,所以管程鎖定規(guī)則不適用;由于value變量沒有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;后面的線程啟動、 終止、 中斷規(guī)則和對象終結(jié)規(guī)則也和這里完全沒有關(guān)系。 因為沒有一個適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無從談起,因此我們可以判定盡管線程A在操作時間上先于線程B,但是無法確定線程B中“getValue()”方法的返回結(jié)果,換句話說,這里面的操作不是線程安全的。

那怎么修復這個問題呢?我們至少有兩種比較簡單的方案可以選擇:要么把getter/setter方法都定義為synchronized方法,這樣就可以套用管程鎖定規(guī)則;要么把value定義為volatile變量,由于setter方法對value的修改不依賴value的原值,滿足volatile關(guān)鍵字使用場景,這樣就可以套用volatile變量規(guī)則來實現(xiàn)先行發(fā)生關(guān)系。

通過上面的例子,我們可以得出結(jié)論:一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”,那如果一個操作“先行發(fā)生”是否就能推導出這個操作必定是“時間上的先發(fā)生”呢?很遺憾,這個推論也是不成立的,一個典型的例子就是多次提到的“指令重排序”,演示例子如下代碼所示:

//以下操作在同一個線程中執(zhí)行
int i=1;
int j=2;

以上代碼的兩條賦值語句在同一個線程之中,根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因為我們在這條線程之中沒有辦法感知到這點。
上面兩個例子綜合起來證明了一個結(jié)論:時間先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾,一切必須以先行發(fā)生原則為準。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容