首頁  >  文章  >  Java  >  【死磕Java並發】-----Java記憶體模型之重排序

【死磕Java並發】-----Java記憶體模型之重排序

黄舟
黄舟原創
2017-02-24 10:05:261162瀏覽

執行程式時,為了提供效能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎麼排序就怎麼排序,它需要滿足以下兩個條件: 

1. 在單執行緒環境下不能改變程式運作的結果;
2. 存在資料依賴關係的不允許重排序

如果看過LZ上篇部落格的就會知道,其實這兩點可以歸結於一點:無法透過happens-before原則推導出來的,JMM允許任意的排序。

as-if-serial語意

as-if-serial語意的意思是,所有的運算都可以為了最佳化而被重新排序,但你必須要確保重排序後執行的結果不能改變,編譯器、runtime、處理器都必須遵守as-if-serial語意學。注意as-if-serial只保證單執行緒環境,多執行緒環境下無效。

下面我們用一個簡單的範例來說明:

int a = 1 ;      //A
int b = 2 ;      //B
int c = a + b;   //C

A、B、C三個運算存在如下關係:A、B不存在資料依賴關係,A和C、B和C存在資料依賴關係,因此在進行重排序的時候,A、B可以隨意排序,但是必須位於C的前面,執行順序可以是A –> B –> C或B –> A –> C。但無論是何種執行順序最終的結果C總是等於3。

as-if-serail語意把單執行緒程式保護起來了,它可以保證在重新排序的前提下程式的最終結果始終都是一致的。

其實對於上段程式碼,他們存在這樣的happen-before關係:

  1. A happens-before B

  2. B happens -before C

  3. A happens-before C

#1、2是程式順序次序規則,3是傳遞性。但是,不是說透過重排序,B可能會排在A之前執行麼,為何還會存在存在A happens-beforeB呢?這裡再次申明A happens-before B不是A一定會在B之前執行,而是A的對B可見,但是相對於這個程式A的執行結果不需要對B可見,且他們重排序後不會影響結果,所以JMM不會認為這種重排序非法。

我們需要明白這一點:在不改變程式執行結果的前提下,盡可能提高程式的運作效率。

下面我們在看一段有意思的程式碼:

public class RecordExample1 {
    public static void main(String[] args){        
    int a = 1;        
    int b = 2;        
    try {
            a = 3;           //A
            b = 1 / 0;       //B
        } catch (Exception e) {

        } finally {
            System.out.println("a = " + a);
        }
    }
}

依照重排序的規則,操作A與操作B有可能會重排序,如果重排序了,B會拋出例外( / by zero),此時A語句一定會執行不到,那麼a還會等於3麼?如果按照as-if-serial原則它就改變了程式的結果。其實JVM對異常做了一種特殊的處理,為了確保as-if-serial語義,Java異常處理機制對重排序做了一種特殊的處理:JIT在重排序時會在catch語句中插入錯誤代償程式碼(a = 3),這樣做雖然會導致cathc裡面的邏輯變得複雜,但是JIT優化原則是:盡可能地優化程式正常運行下的邏輯,哪怕以catch塊邏輯變得複雜為代價。

重排序對多執行緒的影響

在單執行緒環境下由於as-if-serial語義,重排序無法影響最終的結果,但是對於多執行緒環境呢?

如下程式碼(volatile的經典用法):

public class RecordExample2 {
    int a = 0;    boolean flag = false;    
    /**
     * A线程执行
     */
    public void writer(){
        a = 1;                  
        // 1
        flag = true;            
        // 2
    }    /**
     * B线程执行
     */
    public void read(){        
    if(flag){                  
    // 3
           int i = a + a;          
           // 4
        }
    }

}

A執行緒執行writer(),執行緒B執行read(),執行緒B在執行時能否讀到 a = 1 呢?答案是不一定(註:X86CPU不支援寫寫重排序,如果是在x86上面操作,這個一定會是a=1,LZ搞了好久都沒有測試出來,最後查資料才發現 )。

由於操作1 和操作2 之間沒有資料依賴性,所以可以進行重排序處理,操作3 和操作4 之間也沒有資料依賴性,他們亦可以進行重排序,但是操作3 和操作4 之間存在控制依賴關係。假如操作1 和操作2 之間重排序:

【死磕Java並發】-----Java記憶體模型之重排序

按照這種執行順序執行緒B肯定讀不到執行緒A設定的a值,這裡多執行緒的語義就已經被重排序破壞了。

操作3 和操作4 之間也可以重新排序,這裡就不闡述了。但是他們之間存在著一個控制依賴的關係,因為只有操作3 成立操作4 才會執行。當程式碼中存在控制依賴性時,會影響指令序列的執行的平行度,所以編譯器和處理器會採用猜測執行來克服控制依賴對平行度的影響。假如操作3 和操作4重排序了,操作4 先執行,則先會把計算結果暫時保存到重排序緩衝中,當操作3 為真時才會將計算結果寫入變數i中

透過上面的分析,重排序不會影響單執行緒環境的執行結果,但是會破壞多執行緒的執行語意

 以上就是【死磕Java並發】-----Java記憶體模型之重排序的內容,更多相關內容請關注PHP中文網(www.php.cn)!


#
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn