1. 程式人生 > >【搞定Java併發程式設計】第12篇:happens-before

【搞定Java併發程式設計】第12篇:happens-before

 上一篇:final域的記憶體語義:https://blog.csdn.net/pcwl1206/article/details/84925372

目  錄:

1、JMM的設計

2、happens-before的定義

3、happens-before規則


其實我們在前面的Java記憶體模型中已經講過happens-before。但是因為happens-before是JMM最核心的概念。所以這裡再做下詳細的講解。本文內容主要來自於《Java併發程式設計的藝術》一書,算是把自己的讀書筆記記錄在此。

1、JMM的設計

JMM設計時一方面要為程式設計師提供足夠強的記憶體可見性保證;另一方面對編譯器和處理器的限制要儘可能的放鬆,設計JMM時需要進行平衡。

【案例1】

double pi = 3.14;          // A
double r = 1.0;            // B
double area = pi * r *r;   // C

上面計算圓的面積的示例程式碼存在3個happens-before關係,如下所示:

1、A  happens-before  B;

2、B  happens-before  C;

3、A  happens-before  C。

上面的3個happens-before關係,2和3是必須的,但是1不是必須的。因此,JMM把happens- before要求禁止的重排序分為了下面兩類:

1、會改變程式執行結果的重排序;

2、不會改變程式執行結果的重排序。

JMM的設計示意圖如下所示:

JMM的設計示意圖

從上圖可以看出兩點:

        JMM向程式設計師提供的happens- before規則能滿足程式設計師的需求。JMM的happens- before規則不但簡單易懂,而且也向程式設計師提供了足夠強的記憶體可見性保證(有些記憶體可見性保證其實並不一定真實存在,比如上面的A happens- before B)。

        JMM對編譯器和處理器的束縛已經儘可能的少。從上面的分析我們可以看出,JMM其實是在遵循一個基本原則:只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。比如,如果編譯器經過細緻的分析後,認定一個鎖只會被單個執行緒訪問,那麼這個鎖可以被消除。再比如,如果編譯器經過細緻的分析後,認定一個volatile變數僅僅只會被單個執行緒訪問,那麼編譯器可以把這個volatile變數當作一個普通變數來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率。


2、happens-before的定義

JSR使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個執行緒之內,也可以在不同的執行緒之間。因此,JMM可以通過happens-before關係向程式設計師提供跨執行緒的記憶體可見性保證(如果A執行緒的寫操作a與B執行緒的讀操作b之間存在happens-before關係,儘管a操作和b操作在不同的執行緒中執行,但JMM向程式設計師保證a操作將對b操作可見)。

happens-before定義:

1、如果一個操作 happens-before 另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前;

2、兩個操作之間存在 happens-before關係,並不意味著Java平臺的具體實現必須要按照 happens-before關係指定的順序執行。只要重排序的執行結果與按照 happens-before執行的結果一致,那麼JMM允許這種重排序。

第一點是JMM對程式設計師的承諾,第二點是JMM對編譯器和處理器重排序的約束原則。


3、happens-before規則

1、程式順序原則:即在一個執行緒內必須保證語義序列性,也就是說按照程式碼順序執行;

2、鎖規則:解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前。也就是說,如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖);

3、volatile規則:volatile變數的“寫”先發生於“讀”,這保證了volatile變數的可見性。簡單的理解就是:volatile變數在每次被執行緒訪問時,都強迫從主記憶體中讀該變數的值;而當該變數值發生變化時,又會強迫將最新的值重新整理到主記憶體中。因此,任何時刻,不同的執行緒總是能夠看到該變數的最新值;

4、執行緒啟動規則:執行緒的start()方法先於它的每一個動作,即如果執行緒A在執行執行緒B的start方法之前修改了共享變數的值,那麼當執行緒B執行start方法時,執行緒A對共享變數的修改對執行緒B是可見的;

5、傳遞性:A先於B ,B先於C ,那麼A必然先於C;

6、執行緒終止規則:執行緒的所有操作先於執行緒的終結,Thread.join()方法的作用是等待當前執行的執行緒終止。假設線上程B終止之前,修改了共享變數,執行緒A從執行緒B的join方法成功返回後,執行緒B對共享變數的修改將對執行緒A可見。

7、執行緒中斷規則:對執行緒 interrupt() 方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測執行緒是否中斷。

8、物件終結規則:物件的建構函式執行(物件的初始化),結束先於finalize()方法;

這裡從中挑幾種講解下:

  • volatile規則

happens-before關係的示意圖
  • 1 happens-before 2 和 3 happens-before 4由程式的順序規則產生;
  • 2 happens-before 3 是由volatile規則產生;
  • 1 happens-before 4 是由傳遞性規則產生的。

  • 執行緒啟動規則

假設執行緒A在執行過程中,通過執行ThreadB.start()來啟動執行緒B;同時,假設執行緒A在執行ThreadB.start()之前修改了一些共享變數,執行緒B在開始執行後會讀這些共享變數。

  • 1  happens-before  2 由程式順序規則產生;
  • 2  happens-before  4 由start()規則產生;
  • 1  happens-before  4 由傳遞性產生。

這就意味著,執行緒A在執行ThreadB.start()之前對共享變數所做的修改,接下來線上程B開始執行後都將確保對執行緒B可見。


  • 執行緒終止規則 / join() 

假設執行緒A在執行過程中,通過執行ThreadB.join()來等待執行緒B的終止;同時,假設執行緒B在終止之前修改了一些共享變數,執行緒A從ThreadB.join()返回後讀這些共享變數。

  • 2  happens-before  4:join()規則產生;
  • 4  happens-before  5:程式順序規則產生;
  • 2  happens-before  5:傳遞性規則產生。

這也就意味著,執行緒A執行操作ThreadB.join()併成功返回後,執行緒B中的任意操縱都將對執行緒A可見。


  上一篇:final域的記憶體語義:https://blog.csdn.net/pcwl1206/article/details/84925372