1. 程式人生 > 實用技巧 >Java記憶體模型(一)

Java記憶體模型(一)

1. JMM基礎——執行緒間的通訊

處理併發程式設計,首先要處理的問題就是執行緒間如何通訊並且如何做到同步,java的併發實現採用的是共享記憶體模型,執行緒之間的通訊是隱式進行、對開發者完全透明的。

Java記憶體模型(JMM)控制執行緒之間的通訊,決定一個執行緒對共享變數的寫入什麼時候對另一個執行緒可見。

JMM記憶體模型結構如下:

可以看到,如果執行緒A與執行緒B通訊的話,需要經歷兩步:

1)執行緒A把更新的共享變數刷回主記憶體

2)執行緒B去主記憶體讀取執行緒A更新過的變數

2. 重排序

2.1 JMM重排序規則

重排序指編譯器和處理器為了優化程式效能而對指令序列重新排序的一種手段。

1)編譯器:禁止特定型別的編譯器重排序

2)處理器:插入記憶體屏障

從原始碼到最終執行的指令序列示意圖如下:

處理器對記憶體的讀寫順序不一定與記憶體中實際的讀寫順序一致,因為有寫緩衝區的存在,寫緩衝區僅對其所在的處理器可見。

  • 資料依賴性

在兩個操作中,如果其中有一個操作為寫操作,那麼這兩個操作具有資料依賴性,因為只要更改兩個操作的執行順序,程式的執行結果就會改變,為了保證執行結果的正確性,編譯器不會的對具有資料依賴性的操作重排序。

如果重排序對程式的執行結果沒有影響,JMM就會認為這種重排序是合法的。

2.2 記憶體屏障

在多執行緒環境裡需要使用某種技術來使程式結果儘快可見。先假定一個事實:一旦記憶體資料被推送到快取,就會有訊息協議來確保所有的快取會對所有的共享資料同步並保持一致。這個使記憶體資料對CPU核可見的技術被稱為記憶體屏障或記憶體柵欄。

記憶體屏障型別:

  • LoadLoad 屏障

序列:Load1,Loadload,Load2

確保Load1所要讀入的資料能夠在被Load2和後續的load指令訪問前讀入。通常能執行預載入指令或/和支援亂序處理的處理器中需要顯式宣告Loadload屏障,因為在這些處理器中正在等待的載入指令能夠繞過正在等待儲存的指令。 而對於總是能保證處理順序的處理器上,設定該屏障相當於無操作。

  • StoreStore 屏障

序列:Store1,StoreStore,Store2

確保Store1的資料在Store2以及後續Store指令操作相關資料之前對其它處理器可見(例如向主存重新整理資料)。通常情況下,如果處理器不能保證從寫緩衝或/和快取向其它處理器和主存中按順序重新整理資料,那麼它需要使用StoreStore屏障。

  • LoadStore 屏障

序列: Load1,LoadStore,Store2

確保Load1的資料在Store2和後續Store指令被重新整理之前讀取。在等待Store指令可以越過loads指令的亂序處理器上需要使用LoadStore屏障。

  • StoreLoad 屏障(全能型)

序列: Store1,StoreLoad,Load2

確保Store1的資料在被Load2和後續的Load指令讀取之前對其他處理器可見。StoreLoad屏障可以防止一個後續的load指令不正確的使用了Store1的資料,而不是另一個處理器在相同記憶體位置寫入一個新資料。Storeload屏障在幾乎所有的現代多處理器中都需要使用,但通常它的開銷也是最昂貴的。它們昂貴的部分原因是它們必須關閉通常的略過快取直接從寫緩衝區讀取資料的機制。

2.3 happens-before原則

如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作間必須存在happens-before原則。

PS:happens-before原則的定義很微妙,僅僅要求前一個操作對後一個操作可見前一個操作按順序排在後一個操作之前但並不意味前一個操作會在後一個操作之前執行

happens-before原則:

  • 程式順序規則:一個執行緒中的每個操作,h-b與改執行緒的任意後續操作
  • 監視器鎖規則:對一個鎖的解鎖,h-b於對這個鎖的加鎖
  • voliate變數規則:對一個voliate域的寫,h-b於任意對這個voliate域的讀
  • 傳遞性:A h-b B,B h-b C,那麼A h-b C.

2.4 重排序對多執行緒的影響

class Example{
    int a = 0;
    bool flag = false;
    
    public void writer(){
        a = 1;          //1
        flag = true;    //2
    }
    
    public void reader(){
        if(falg){           //3
            int i = a * a;  //4
        }
    }
}

假設有兩個執行緒A和B,A先執行writer()方法,隨後B執行reader()方法,執行緒B在執行操作4時,能否看到執行緒A操作2的寫入呢?答案是不一定。因為操作1和操作2沒有資料依賴關係,所以可以對這兩個操作重排序,執行時序圖如下:

由時序圖可以看出,多執行緒的語義被破壞。

操作3和操作4同樣沒有資料依賴關係,但是具有控制依賴關係,下面是對操作3和操作4重排序的時序圖:

可以看出,提前猜測實質上是對操作3和操作4做了重排序,同樣破壞了多執行緒的語義。