Java多線程之先行發生原則(happens-before)

本文首發於微信公衆號:老胡碼字java

前面介紹了Java內存模型及內存屏障相關概念,這篇文章接着介紹多線程編程另一個比較重要的概念:先行發生原則(happens-before)。編程

重要性

happens-before是判斷數據是否存在競爭,線程是否安全的主要依據,經過這個原則,咱們能夠解決併發環境下兩個操做之間是否可能存在衝突的全部問題。安全

happens-before是什麼

它Java內存模型中針對兩項操做定義的偏序關係。例如操做A先行於操做B發生,那麼操做B能夠觀察到操做A所產生的全部影響,這些影響包括修改內存中共享變量的值、發送的消息,調用的方法等。微信

舉個例子:多線程

//該操做在線程1中執行
i = 1;

//該操做在線程2中執行
j = i;

//該操做在線程3中執行
i = 2
複製代碼

上述例子中有共享變量i和j,假設線程1中執行的操做「i=1」先於線程2中的操做「j=i」發生,那共享變量j的值確定爲1。併發

得出這個結論是由於根據happens-before原則:「i=1」操做能夠被觀察到,而線程3尚未開始。app

若是依舊假定操做「i=1」和操做「j=i」的先行發生關係,而線程3開始於線程1和線程2之間,線程3與線程2之間沒有先行發生關係,最後變量j的值是多少呢?結果多是1也多是2。這種狀況線程3對變量i的影響可能會被線程B觀察到,也可能不會被觀察到,當沒有被線程B觀察到的時候,線程B就會讀取到過時的舊數據,這個時候就出現了多線程安全性問題。this

happens-before的8條規則

Java內存模型中已經存在8條定義好的先行發生規則,這些先行發生規則不須要任何同步操做就已經存在,若是兩個操做之間的關係不在這幾天規則或則沒法經過這幾條規則推導出來,那這兩個操做就沒有順序保障,虛擬機就能夠對他們進行重排。spa

具體操做以下:線程

  1. 程序次序規則:在同一個線程中,按照程序代碼順序,寫在前面的操做先行發生與寫在後面操做(控制流順序:分支、循環等)。
  2. 鎖規則:一個unlock操做先行發生於後面(時間上)對同一個鎖的lock操做
  3. volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做
  4. 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個操做
  5. 線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測(能夠經過Thread.join()等待線程結束、Thread.isAlive()返回值)。
  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的檢測到中斷事件的代碼
  7. 對象終結規則:一個對象的初始化完成先行發生於對象的finalize()方法的開始
  8. 傳遞性規則:若是操做A先行發生於操做B,操做B先行發生於操做C,那操做A就先行發生於操做C

happens-before規則示例

下面經過一個示例來看看如何經過這些規則來判斷操做之間是否具備順序性,而對於讀寫共享變量的操做來講,就是現場是否安全。

private int value = 0;

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

public int getValue() {
    return this.value;
}
複製代碼

這個例子是一段很簡單的代碼,假定有線程1和線程2,線程1先(時間上先)調用"setValue(1)",而後線程2調用"getValue()",最後線程2獲得的值是多少?

咱們根據前面提到的8個規則來分析下:

  • 由於兩個方法分別在線程1和線程2中執行,不是同一個線程,所以程序次序規則不適用
  • 代碼中沒有任何同步代碼塊(鎖),所以鎖規則也不適用
  • 代碼中value變量爲非volatile變量,所以volatile變量規則也不適用
  • 整個執行過程很明顯和線程啓動、終止、中斷、對象終結規則也沒有關係
  • 由於沒有任何先行發生關係,因此傳遞性規則也不適用

根據上面的分析,雖然線程1在操做時機上先於線程2,可是由於沒有任何先行發生關係,因此沒法肯定線程2中"getValue()"的值,所以這兩個線程的操做放在一塊兒是不安全的。

要解決這個問題也很簡單,一種是將setValue和getValue兩個方法都定義爲synchronized方法,這樣就能夠套用鎖規則,另一種是將value變量定義爲volatile變量,並且這裏修改value值的時候不依賴value的原值,因此就能夠套用volatile變量規則。

經過上面的分析,咱們能夠知道一個操做「時間上的先發生」不表明這個操做會「先行發生」。

那一個操做「先行發生」是否是「時間上也是先發生」呢,這個其實也是不能保證的,典型的例子就是指令重排,例以下面的例子:

//下面兩個操做在同一個線程中執行
int i = 1;  //操做1
int j = 2;  //操做2
複製代碼

上面的示例中,按照線程次序規則,操做1先行發生於操做2,可是操做2在實際執行過程當中極可能由於重排序而被處理器先執行,這樣也沒有影響先行發生原則的正確性,由於在這個線程中咱們沒法感知到這種變化。

總結

時間上的前後順序與先行發生原則之間沒有基本的關係,所以咱們在衡量線程安全與否時不要關注時間順序,而是應該關注先行發生原則。



下面是個人我的公衆號,歡迎關注交流
相關文章
相關標籤/搜索