1. 程式人生 > >(八 附)java併發程式設計--JVM之指令重排分析

(八 附)java併發程式設計--JVM之指令重排分析

  引言:在Java中看似順序的程式碼在JVM中,可能會出現編譯器或者CPU對這些操作指令進行了重新排序;在特定情況下,指令重排將會給我們的程式帶來不確定的結果…..

1.什麼是指令重排?

  在計算機執行指令的順序在經過程式編譯器編譯之後形成的指令序列,一般而言,這個指令序列是會輸出確定的結果;以確保每一次的執行都有確定的結果。但是,一般情況下,CPU和編譯器為了提升程式執行的效率,會按照一定的規則允許進行指令優化,在某些情況下,這種優化會帶來一些執行的邏輯問題,主要的原因是程式碼邏輯之間是存在一定的先後順序,在併發執行情況下,會發生二義性,即按照不同的執行邏輯,會得到不同的結果資訊。

2. 資料依賴性

  主要指不同的程式指令之間的順序是不允許進行互動的,即可稱這些程式指令之間存在資料依賴性。

主要的例子如下:
[html] view plain copy
名稱 程式碼示例 說明
寫後讀 a = 1;b = a; 寫一個變數之後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變數之後,再寫這個變數。
讀後寫 a = b;b = 1; 讀一個變數之後,再寫這個變數。

  進過分析,發現這裡每組指令中都有寫操作,這個寫操作的位置是不允許變化的,否則將帶來不一樣的執行結果。
  編譯器將不會對存在資料依賴性的程式指令進行重排,這裡的依賴性僅僅指單執行緒情況下的資料依賴性;多執行緒併發情況下,此規則將失效。

3. as-if-serial語義

  不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。
分析: 關鍵詞是單執行緒情況下,必須遵守;其餘的不遵守。

程式碼示例:
[html] view plain copy
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
分析程式碼: A->C B->C; A,B之間不存在依賴關係; 故在單執行緒情況下, A與B的指令順序是可以重排的,C不允許重排,必須在A和B之後。
結論性的總結:
as-if-serial語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器,runtime 和處理器共同為編寫單執行緒程式的程式設計師建立了一個幻覺:單執行緒程式是按程式的順序來執行的。as-if-serial語義使單執行緒程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題。
核心點還是單執行緒,多執行緒情況下不遵守此原則。

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=true將被reader的方法體中看到,並正確的設定結果。 但是在多執行緒情況下,是否還是隻有一個確定的結果呢?
  假設有A和B兩個執行緒同時來執行這個程式碼片段, 兩個可能的執行流程如下:
  可能的流程1, 由於1和2語句之間沒有資料依賴關係,故兩者可以重排,在兩個執行緒之間的可能順序如下:

  可能的流程2:, 在兩個執行緒之間的語句執行順序如下:

  根據happens- before的程式順序規則,上面計算圓的面積的示例程式碼存在三個happens- before關係:
A happens- before B;
B happens- before C;
A happens- before C;
  這裡的第3個happens- before關係,是根據happens- before的傳遞性推匯出來的
  在程式中,操作3和操作4存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。為此,編譯器和處理器會採用猜測(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜測執行為例,執行執行緒B的處理器可以提前讀取並計算a*a,然後把計算結果臨時儲存到一個名為重排序緩衝(reorder buffer ROB)的硬體快取中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變數i中。從圖中我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這裡破壞了多執行緒程式的語義。
  核心點是:兩個執行緒之間在執行同一段程式碼之間的critical area,在不同的執行緒之間共享變數;由於執行順序、CPU編譯器對於程式指令的優化等造成了不確定的執行結果。

5. 指令重排的原因分析

  主要還是編譯器以及CPU為了優化程式碼或者執行的效率而執行的優化操作;應用條件是單執行緒場景下,對於併發多執行緒場景下,指令重排會產生不確定的執行效果。

6. 如何防止指令重排

  volatile關鍵字可以保證變數的可見性,因為對volatile的操作都在Main Memory中,而Main Memory是被所有執行緒所共享的,這裡的代價就是犧牲了效能,無法利用暫存器或Cache,因為它們都不是全域性的,無法保證可見性,可能產生髒讀。
  volatile還有一個作用就是區域性阻止重排序的發生,對volatile變數的操作指令都不會被重排序,因為如果重排序,又可能產生可見性問題。
  在保證可見性方面,鎖(包括顯式鎖、物件鎖)以及對原子變數的讀寫都可以確保變數的可見性。但是實現方式略有不同,例如同步鎖保證得到鎖時從記憶體裡重新讀入資料重新整理快取,釋放鎖時將資料寫回記憶體以保資料可見,而volatile變數乾脆都是讀寫記憶體。

7. 可見性

  這裡提到的可見性是指前一條程式指令的執行結果,可以被後一條指令讀到或者看到,稱之為可見性。反之為不可見性。這裡主要描述的是在多執行緒環境下,指令語句之間對於結果資訊的讀取即時性。

8. 參考文獻