1. 程式人生 > >java併發程式設計一一多執行緒執行緒安全(四)

java併發程式設計一一多執行緒執行緒安全(四)

##1.java重排序
###1.1資料依賴性
如果兩個操作訪問同一個變數時,且這兩個操作匯中有一個為寫操作,此時這兩個操作之間就
存在資料依賴性。資料依賴分下列三種類型。

名稱 程式碼示例 說明
寫後讀 a=1; b=a; 寫一個變數之後,再讀這個變數
寫後寫 a=1; a=2; 寫一個變數之後,再寫這個變數
讀後寫 a=b; b=1; 讀一個變數之後,再寫這個變數

上面這三種情況,只要重排序兩個操作的執行順序,程式的執行結果將會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器做重排序時,
會遵守資料依賴性,編譯器和處理器不會改變存在資料依賴性關係的兩個操作的執行
順序。
注意,這裡所說的資料依賴性僅針對單個處理器中執行的指令序列和單個執行緒中執行的
操作,不同處理器之間和不同執行緒之間的資料依賴性不被編譯器和處理考慮。

###1.2as-if-serial語義

  1. as-if-serial 語義的意思指:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)
    程式的執行結果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語義。
  2. 為了遵守as-if-serial語義編譯器和處理器不會對資料依賴性關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴性,這些操作可能被編譯器和處理器重排序。具體請看下面程式碼示例:
    double pi = 3.14; // A
    double r = 1.0; // B
    double area = pi * r *r; //C
    上面三個操作的資料依賴性如下圖所示:
    這裡寫圖片描述
    如上圖所示,A和C之間存在資料依賴性,同時A和C之間也存在資料依賴性關係。因此在
    最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程式的結果將
    會被改變)。但A和B之間沒有資料依賴性關係,編譯器和處理器可以重排序A和B之間的執行順序。
    下圖是該程式的兩種執行順序:
    這裡寫圖片描述

    as-if-serial 語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器,runtime 和處理器共同為
    編寫單執行緒程式的程式設計師建立了一個幻覺:單執行緒程式是按程式的順序來執行的。as-if-serial語義使
    單執行緒程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題。

###1.3程式順序規則
根據happens-before 的程式規則,上面計算圓的面積的例項程式碼存在三個happens-before關係:

  1. A happens-before B;
  2. B happens-before C;
  3. A happens-before C;
    這裡的第三個happens-before關係,是根據happens-before的傳遞性推倒出來的。
    這裡 A happens-before B ,但是實際執行時 B 卻可以排在 A之前執行(看上面的重排序後的執行
    順序)。在第一章提到過,如果 A happens-before B,JMM並不要求A一定要在B之前執行。JMM
    僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作順序排在第二個操作之前.
    這裡操作A 的執行結果不需要對操作B 可見;而且重排序操作A 和操作B 後的執行結果,與操作A
    和操作B 案happens-before 順序執行的結果一致。在這種情況下,JMM會認為這種重排序並不非法
    (not illegal),JMM允許這種重排序。
    在計算機中,軟體技術和硬體技術有一個共同的目標:在不改變程式執行結果的前提下,儘可能的開發並行度。
    編譯器和處理器遵從這一目標,從happens-before的定義我們可以看出,JMM也同樣遵從這一目標。

###1.4重排序對多執行緒的影響
現在讓我們來看看,重排序是否對改變多執行緒序的執行結果。請看下面的示例程式碼:

class ReorderExample {
	int a = 0;
	boolean flag = false;
	
	public void writer() {
	    a = 1;                   //1
	    flag = true;             //2
	}
	
	Public void reader() {
	    if (flag) {                //3
	        int i =  a * a;        //4
	        ……
	    }
	}
}

flag 變數是個標記,用來標識變數a是否已被寫入。這裡假設有兩個執行緒A 和B ,A首先執行writer()
方法,隨後B執行緒接著執行reader()方法。執行緒B在執行操作4 時,能否看到執行緒A 在操作 1 對共享啊的
寫入?
答案是不一定能看到。
由於操作1和操作2沒有資料依賴性,編譯器和出來器可以對著兩個操作重排序;同樣操作3和操作4
沒有資料依賴性,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2
重排序時,可能會產生什麼效果?情況下面的程式執行時序圖:
這裡寫圖片描述
如上圖所示,操作1 和操作2 做了重排序。程式執行時,執行緒A首先先寫標記變數flag,隨後執行緒B讀這個變數。
由於條件判斷為真,執行緒B 將讀取變數a。此時, 變數a 還根本沒有被執行緒A寫入,在這裡多執行緒chengx
的語義就被重排序破壞了!

注意: 本文重中統一用紅色的虛箭頭表示錯誤的讀操作,用綠色的虛箭頭表示正確的讀操作。

下面再看看,當操作3和操作4 重排序時會產生什麼效果(藉助這個重排序,可以順便說明控制依賴性)
下面是操作3 和操作4 重排序後,程式的執行時序圖:
這裡寫圖片描述
在程式中,操作3 和操作4 存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行
的並行度。為此,編譯器和處理器會採用猜測(speculation)執行來克服控制相關性對並行度的
影響。以處理器的猜測執行為例,執行執行緒B的處理器可以提前讀取並計算a*a,然後把計算結果
臨時儲存到一個名為重排序緩衝(reorder buffer ROB)的硬體快取中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變數i中。

從圖中我們可以看出,猜測執行實質上對操作3 和操作4 做了重排序。重排序在這裡破壞了多執行緒程式
的語義!在單執行緒程式中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義
允許對存在在控制依賴的操作做重排序的原因);但在多執行緒程式中,對存在控制依賴的操作重排序,
可能會改變程式的執行結果。