先行發生原則

若是Java內存模型中全部的有序性都僅僅靠volatile和synchronized來完成,那麼有一些操做將會變得很煩瑣,可是咱們在編寫Java併發代碼的時候並無感受到這一點,這是由於 Java語言中有一個「先行發生」(happens-before)的原則。這個原則很是重要,它是判斷數據 是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則一攬子地 解決併發環境下兩個操做之間是否可能存在衝突的全部問題。
如今就來看看「先行發生」原則指的是什麼。安全

先行發生是Java內存模型中定義的兩項操做 之間的偏序關係,若是說操做A先行發生於操做B,其實就是說在發生操做B以前,操做A產 生的影響能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了 方法等。這句話不難理解,但它意味着什麼呢?咱們能夠舉個例子來講明一下,如代碼清單 12-8中所示的這3句僞代碼。 多線程

代碼清單12-8 先行發生原則示例
//如下操做在線程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內存模型下一些「自然的」先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從 下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。 app

程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序, 由於要考慮分支、循環等結構。函數

管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock 操做。這裏必須強調的是同一個鎖,而「後面」是指時間上的前後順序。this

volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。 編碼

線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。 spa

線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。線程

線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。 對象

對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。

傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

Java語言無須任何同步手段保障就能成立的先行發生規則就只有上面這些了,筆者演示一下如何使用這些規則去斷定操做間是否具有順序性,對於讀寫共享變量的操做來講,就是線程是否安全,讀者還能夠從下面這個例子中感覺一下「時間上的前後順序」與「先行發生」之 間有什麼不一樣。
演示例子如代碼清單12-9所示。
代碼清單12-9先行發生原則示例 
  private int value=0;
  pubilc void setValue(int value){
    this.value=value;
  }
  public int getValue(){
    return value;
  }
代碼清單12-9中顯示的是一組再普通不過的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變量規則來實現先行發生關係。

經過上面的例子,咱們能夠得出結論:一個操做「時間上的先發生」不表明這個操做會是「先行發生」,那若是一個操做「先行發生」是否就能推導出這個操做一定是「時間上的先發生」呢?很遺憾,這個推論也是不成立的,一個典型的例子就是屢次提到的「指令重排序」, 演示例子如代碼清單12-10所示。

代碼清單12-10先行發生原則示例 
//如下操做在同一個線程中執行
int i=1;

int j=2;
代碼清單12-10的兩條賦值語句在同一個線程之中,根據程序次序規則,「int i=1」的操做先行發生於「int j=2」,可是「int j=2」的代碼徹底可能先被處理器執行,這並不影響先行發生原則的正確性,由於咱們在這條線程之中沒有辦法感知到這點。

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

相關文章
相關標籤/搜索