happen before 原則


並發一直都是程序開發者繞不開的難題,在上一篇文章中我們知道了導致並發問題的源頭是 : 多核 CPU 緩存導致程序的可見性問題、多線程間切換帶來的原子性問題以及編譯優化帶來的順序性問題。

 

原子性問題我們暫且不談,Java 中有足夠健壯的鎖機制來保證程序的原子性,后面學習的重點也是在這方面。今天我們就先來看看 Java 是怎么解決可見性與順序性問題的。

 

合理的建議是 按需禁用緩存與編譯優化,但是怎么才算是按需禁用呢?這就要具體問題具體分析了。Java 內存模型也規定了 JVM 如何按需提供禁用緩存與編譯優化的方法,這些方法就是 volatile、syncronized、final 三個關鍵字與 happen-before 原則。

 

  • volatile

 

volatile 其實是一種稍弱的同步機制,在並發場景下它並不能保證線程安全。

 

加鎖機制既可以保證可見性又可以保證原子性,而 volatile 變量則只能保證可見性。

 

在 jdk1.5 之前 volatile 給我們最深刻的印象就是 禁用緩存,即每次讀寫都是直接操作內存(不是 CPU 緩存),從內存中取值。Java 從 jdk1.5 開始對 volatile 進行了優化, 我們先來看下面的例子,再討論優化了什么和怎么優化的。

 

假設有線程 A 和線程 B,A 線程調用 write(),將 flag 賦值為 true,B 線程調用 read() ,根據 volatile 定義變量的可見性,B 線程中 flag 為 true 所以會進入if 判斷,那么此時的輸出 index 的值是多少呢?

class VolatileTest{
 int index = 0; 
 volatile boolean flag = false;
 
 public void write(){
   index  = 10;  
   flag = true;
 }
​
public void read(){
  if(flag){
    System.out.println(index);
  }
}
}

這個問題在 jdk1.5 之前,顯示的結果可能為 0,也可能為 10,原因在於 CPU 緩存導致的可見性問題,flag 是可以保證可見性,但是 index 卻無法保證,當 A 線程執行完寫進 CPU 緩存還沒有更新的內存時,此時 B 線程讀出的 index 值就是 0。

 

為了解決上述問題,從 jdk1.5 開始對 volatile 修飾的變量進行了優化, 在 jdk1.5 以后 此時 index 輸出結果就是 10 。

 

到底是怎么優化的呢? 答案是 happen-before 原則中的傳遞性規則。

 

  • happen before 原則

 

happen before 並不是字面的英文的意思,想要理解它必須知道 happen before 的含義,它真正的意思是前面的操作對后續的操作都是可見的,比如 A happen before B 的意思並不是說 A 操作發生在 B 操作之前,而是說 A 操作對於 B 操作一定是可見的。

 

知道了它的意思,我們再來看一下 happen before 原則中與開發相關的幾項規則:

 

  • 程序的順序性規則

 

這條規則很簡單,也符合我們常規的思維,就是說在一個線程中程序的執行順序,前面的操作對后面的操作是可見的,同樣是上面的代碼,在執行 write() 方法時,index = 10 對 flag = true 是可見的,如下 :

 

class VolatileTest{
 int index = 0; 
 volatile boolean flag = false;
 
 public void write(){
   index  = 10;  // index 賦值 對 flag 是可見的
   flag = true;
 }
​
public void read(){
  if(flag){
    System.out.println(index);
  }
}
}
  • volatile 變量規則

 

 volatile 修飾的變量,對該變量的寫操作一定可見於對該變量的讀操作。

 

同樣是上面的代碼,A 線程執行 write() 為 flag 變量賦值為 true,B 線程執行 read() 方法,那么此時的 flag 一定為true,與 A B 線程執行順序無關。

 

  • 傳遞性

 

傳遞性就是 jdk1.5 之后對 volatile 語義的增強。啥叫傳遞性呢 ? 

 

           舉個簡單的數學比大小的例子,你就明白了。 

 

有 a、b、c 三個數,如果 a > b, b > c, 那么由傳遞性可知 a > c。

 

這里的傳遞性的意思與例子類似, A 操作 happen before 於 B 操作,B 操作 happen before 於 C 操作,那么 A 操作 happen before 於 C 操作。同樣是上面的代碼,我們來解釋下為什么 在 jdk1.5 之后 B 線程執行 read() 方法打印的結果一定為 10 呢?如圖:

 

 

根據第一條程序的順序性規則可知,A線程中 index = 10 對 flag = true 可見;

 

根據第二條 volatile 規則可知,flag 使用 volatile 修飾的變量, A 線程 flag 寫 對 B 線程 flag 的讀可見;

 

綜合以上兩條規則:

 

index = 10  happen before 於 flag 的寫 ,

 

A 線程 flag 的寫 happen before 於 B 線程 flag 的讀,

 

根據傳遞性 , A 線程 對 index 的寫 happen before 於 B 線程 對 flag 的讀。所以  A 線程對 index 變量的寫操作對 B 線程 flag 變量的讀操作是可見的,即 B 線程讀取 index 的打印結果為 10 。

 

  • 管程中鎖的規則

        

管程是一種通用的同步原語,管程在 java 中指的就是 syncronized。這條規則說的是 線程的解鎖 happen before 於對這個線程的加鎖

 

也就是說線程的解鎖對線程的加鎖是可見的,那么在一個線程中操作的變量,在這個線程解鎖后,根據傳遞性規則,當另一個線程加鎖的時候,就會讀到上一個線程對這個變量的操作后的新值。

 

  • 線程的 start 規則

     

A 線程中調用 B 線程的 start(), 則 start() 操作 happen before 於 B 線程所有操作,也就是說 A 線程調用 start() 操作前對共享變量的賦值,對 B 線程可見,即在 B 線程中能獲取 A 對到共享變量操作后的新值。

 

  • 線程的 join 規則

 

A線程中調用B線程的 join() 操作,如果成功返回 則 B的所有操作對 join() 結果的返回一定是可見的,即在 A 線程能獲取到 B 對共享變量操作后的新值。

 

除了以上列舉幾條規則,happen before 還有些其他的規則,在開發中不常用到,這里我們就不一一列舉了。其實 Java 內存模型大體可以分為兩部分,一部分是編寫並發程序的開發人員,另一部分是 面向JVM 的實現人員 。當然我們更關注前者,而前者的核心就是 happen before 原則,常用的就是上面我們列舉的原則,了解這些也就可以了。

 

在編譯器優化方面使用的最多的就是 final 了, final 修飾變量就是常量,因此編譯器可以使勁的優化,在 jdk1.5 以后 Java 內存模型對 final 類型變量的重排進行了約束。現在只要我們提供正確構造函數沒有“逸出”,就不會出問題了。

 

總結:

 

happen before 原則核心就是可見性,並不是說 一個操作發生在另一個操作前面,它真正要表達的是:前面一個操作的結果對后續操作是可見的。

 

 

參考資料 :   《JAVA 並發編程實戰》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM