硬核!Java內存模型(JMM)

在講解之前,先區別兩個概念:java內存模型與JVM內存模型。

  • java內存模型:JMM(Java Memory Model),JMM的目的是為了解決Java多線程對共享數據的讀寫一致性問題,通過Happens-Before語義定義了Java程序對數據的訪問規則,修正之前由於讀寫衝突導致的Cache數據不一致的問題。這是一種邏輯抽象,並沒有對應內存實體。它規範了(本文將重點講解)
  • JVM內存模型:是指JVM運行過程中數據區域,此為實實在在存在著的內存區域。

上面已講,JMM只是邏輯抽象,沒有與其對應的內存運行區域,故不要將兩者混著學,否則,你會瘋。

Java併發編程遇到的問題

我們在多線程編程中解決的兩個最常見的問題:

  • 多線程之間如何操作同一變量;
  • 多線程中如何處理同步問題。

java是跨平臺語言,不同處理器架中都有自己高速緩存,並處理與主內存的通信協調。不同的處理器架構也都提供了自己的緩存一致性。java為了實現跨平臺的語言特性,在[JSR-133]中提出了JMM規範,用於解決上述複雜的多平臺問題。 JMM決定了一個線程對共享便利那個的寫入何時對另一個線程可見。JMM定義了一個抽象關係:

線程之間的共享變量存儲在主內存中,每個線程都有一個私有的工作內存,工作內存中存儲了共享變量的副本(類似於CPU中高速緩存與內存的關係,其實工作內存包含了高速緩存的概念)。工作內存是一個抽象概念,並不存在於真實內存中。

如圖所示:

硬核!Java內存模型(JMM)

public class Test{
private int i=1;

//線程A修改
public void setVar(){
i=2;
}

//線程B獲取
public int getVar(){
return i;
}
}

線程A修改變量並對線程B可見需要通過以下步驟: 1 .(setVar) 線程A修改本地內存A中的變量副本(A), 並刷新到主內存中(B); 2 . (getVar)線程B從主內存拿取變量值,更新本地內存B中的值

可見性與原子性

上述兩個步驟,如果線程A對變量的修改能夠正確的顯示在線程B中,(即:一個線程修改的狀態對另一個線程是可見的),稱為可見性。 如果要保證上述代碼能夠正確運行,則需要保證步驟1的操作不可被拆分,需要按照:線程A(A->B)->線程B的順序執行,如果出現了線程A(A)->線程B->線程A(B)這樣的順序執行,則會出現獲取數據錯誤的問題。我們需要保證setVar是原子操作,這稱為原子性。

指令重排序

無論是處理器還是JVM,唯一的宗旨就是在保證處理結果正確的前提下,盡最大可能的提高程序運行效率。為了提高運行效率,編譯器,處理器執行期間,處理器高速緩存在回寫主內存時都對運算指令進行了優化重排序。如下代碼(摘自java併發編程藝術):

class ReorderExample {

int a = 0;
boolean flag = false;

//線程A
public void writer() {
a = 1; // 1
flag = true; // 2
}

//線程B
public void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
}

flag變量是個標記,用來標識變量a是否已被寫入。這裡假設有兩個線程A和B,A首先執行writer()方法,隨後B線程接著執行reader()方法。線程B在執行操作4時,線程A拿到的a的值不一定是最新的。看下圖:

硬核!Java內存模型(JMM)

程序在運行過成中,操作1和操作2可能會做了指令重排序,1和2顛倒執行,這在單線程中沒有任何問題,但是在多線程中就會出現錯誤。

happens-before語義

Java內存模型使用了各種操作來定義的,包括對變量的讀寫,監視器的獲取釋放等,JMM中使用了 happens-before語義闡述了操作之間的內存可見性。如果想要保證執行操作B的線程看到操作A的結構(無論AB是否在同一線程),那麼A,B必須滿足 happens-before關係。如果兩個操作之間缺乏 happens-before關係,那麼JVM可以對他們進行任意的重排序。

happens-before規則包括:

  • 程序順序規則。一個線程中,如果操作A在B之前,那麼線程中A操作將在B操作之前執行。
  • 監視器鎖規則。在監視器鎖上的解鎖操作必須在同一個監視器鎖上的佳作之前執行。
  • volatile規則。對volatile變量的寫入操作必須在對改變的讀操作之前進行。
  • 線程啟動規則。在線程上對Thread.start()的調用必須在對線程執行任何操作之前執行。
  • 線程結束規則。線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執行。
  • 終結器規則。對象的構造函數必須在啟動該對象的終結器之前執行完成。
  • 傳遞性。如果操作A在B之前執行,並且操作B在C之前執行,那麼操作A在C之前執行。

我們在寫代碼過程中,當一個變量被多個線程讀取並且被至少一個線程寫入的時候,如果在讀操作與寫操作之前沒有實現happens-before排序,則就會產生數據競爭問題,產生錯誤的結果。


分享到:


相關文章: