1. 程式人生 > 其它 >JVM:Java記憶體模型(JMM)

JVM:Java記憶體模型(JMM)

Java 記憶體模型

JMM:Java Memory Model

與記憶體結構(執行時資料區等)是不同的兩個概念

JMM 用於遮蔽各種硬體和作業系統的記憶體訪問差異,讓 Java 程式在各平臺能達到一致的記憶體訪問效果。

  • 讓 Java 的併發記憶體訪問操作不會產生歧義。
  • JVM 能自由利用硬體的特性,獲取更好的執行速度。

主記憶體與工作記憶體

JMM 定義程式中各種變數的訪問規則

這裡的變數不包括區域性變數和方法引數。

主記憶體:所有變數都儲存在主記憶體。

工作記憶體:每個執行緒有自己的工作記憶體,其中儲存了該執行緒所用變數的主記憶體副本。

  • 執行緒對變數的操作(讀取、賦值)只在工作記憶體中進行,不能直接讀寫主記憶體中的資料。

  • 執行緒之間也無法直接訪問對方工作記憶體的變數,需要通過主記憶體來傳遞。

說明:主記憶體與工作記憶體的概念,與執行時資料區中的堆疊等結構,是不同層次的概念。

兩個概念之間基本沒有任何關係,如果一定要勉強對應

  • 主記憶體:對應堆中的物件例項資料部分
  • 工作記憶體:對應虛擬機器棧的部分割槽域

三大特性

原子性(Atomicity)、可見性(Visibility)、有序性(Ordering)

JMM 是圍繞著併發過程中這三個特性來建立的。

原子性

舉個例子

兩個執行緒,對一個初值為 0 的靜態變數,分別作自增和自減,結果是 0 嗎?

分析位元組碼

  • getstatic:獲取靜態變數 i 的值

  • iconst_1:從常量池載入一個常量 1

  • iadd / isub:加法/減法

  • putstatic:將修改後的值,賦值給靜態變數 i

    // i++ 對應位元組碼
    getstatic i
    iconst_1
    iadd
    putstatic i
        
    // i-- 對應位元組碼
    getstatic i
    iconst_1
    isub
    putstatic i
    

結果可能是正數、負數、0。

在多執行緒情況下,對靜態變數 i 的操作不是原子性。

假如 8 行位元組碼交錯執行,就可能出現結果非 0 的情況。

解決方法

同步關鍵字synchronized

synchronized(Object) {
    // 要求原子性操作的程式碼
}

可見性

舉個例子:退不出的迴圈

執行緒 t 由於 run == true 進入迴圈。

主執行緒將 run 設為 false,但執行緒 t 沒有退出迴圈。

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
            // ....
        }
    });
    t.start();
    Thread.sleep(1000);
    run = false;
}

淺分析一下

  • 執行緒 t 開始時,從主記憶體中將 run 的值讀到工作記憶體。
  • 由於執行緒對變數的操作只在工作記憶體中進行,不能直接讀寫主記憶體中的資料。
  • 主記憶體中的資料改變,而執行緒 t 只是讀取本地記憶體中的 run,沒有從主記憶體中再次讀取 run 值

解決方法

易變關鍵字volatile

  • 修飾成員變數和靜態成員變數
  • 要求執行緒從主記憶體中讀取變數值,而不是從工作快取。

注意:synchronized 也可以保證原子性,但效率低。

有序性

舉個例子:詭異的結果

兩個執行緒分別執行以下兩個方法,最終 num 和 ready 的值是什麼?

int result = 0;

int num = 1;
boolean ready = false;

// 執行緒t1執行
public void m1() {
    if(ready) {
        // ①
        result = num + num;
    } else {
        // ②
        result = 1;
    }
}
// 執行緒t2執行
public void m2() {
    // ③
    num = 2;
    // ④
    ready = true;
}

一共有以下三種情況:結果可能是 1 或 4。

  1. 先進入 m1():條件假,進入 else 執行 ②

  2. 先進入 m2():執行③,④還未執行;此時進入 m1():條件假,執行②

  3. 先進入 m2():執行③④;再進入 m1():條件真,執行①

還有一種情況:result == 2

這是什麼情況呢?先來看看圖示。

  • 先進入 m2():執行④,③未執行

  • 此時進入 m1():條件真,執行①

指令重排:執行期優化的策略,也就是以上的現象。

簡單來說,JVM 會將容易執行的程式碼優先執行。

而程式碼順序的改變,導致了結果的改變。

解決方法

volatile:可以禁用指令重排。

經典應用單例模式——雙重檢查加鎖(double-checked locking)