1. 程式人生 > >jvm 指令重排

jvm 指令重排

clas 圓的面積 變量 read 編譯 spec iter view 程序

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

1. 什麽是指令重排?

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

2. 數據依賴性

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

主要的例子如下:

[html] view plain copy print?
  1. 名稱 代碼示例 說明
  2. 寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
  3. 寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
  4. 讀後寫 a = b;b = 1; 讀一個變量之後,再寫這個變量。
名稱 	代碼示例 	說明
寫後讀 	a = 1;b = a; 	寫一個變量之後,再讀這個位置。
寫後寫 	a = 1;a = 2; 	寫一個變量之後,再寫這個變量。
讀後寫 	a = b;b = 1; 	讀一個變量之後,再寫這個變量。

進過分析,發現這裏每組指令中都有寫操作,這個寫操作的位置是不允許變化的,否則將帶來不一樣的執行結果。

編譯器將不會對存在數據依賴性的程序指令進行重排,這裏的依賴性僅僅指單線程情況下的數據依賴性;多線程並發情況下,此規則將失效。

3. as-if-serial語義

不管怎麽重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。

分析: 關鍵詞是單線程情況下,必須遵守;其余的不遵守。

代碼示例:

[html] view plain copy print?
  1. double pi = 3.14; //A
  2. double r = 1.0; //B
  3. double area = pi * r * r; //C
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. 在多線程下的指令重排

首先我們基於一段代碼的示例來分析,在多線程情況下,重排是否有不同結果信息:

[html] view plain copy print?
  1. class ReorderExample {
  2. int a = 0;
  3. boolean flag = false;
  4. public void writer() {
  5. a = 1; //1
  6. flag = true; //2
  7. }
  8. Public void reader() {
  9. if (flag) { //3
  10. int i = a * a; //4
  11. ……
  12. }
  13. }
  14. }
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關系:

  1. A happens- before B;
  2. B happens- before C;
  3. 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. 參考文獻

    • http://www.infoq.com/cn/articles/java-memory-model-2
    • http://www.cnblogs.com/chenyangyao/p/5269622.html

jvm 指令重排