1. 程式人生 > 其它 >黑馬 jvm(三) java 記憶體模型 (Java Memory Model)JMM

黑馬 jvm(三) java 記憶體模型 (Java Memory Model)JMM

1. java 記憶體模型

很多人將【java 記憶體結構】與【java 記憶體模型】傻傻分不清,【java 記憶體模型】是 Java Memory
Model(JMM)的意思。
關於它的權威解釋,請參考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf?AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b
簡單的說,JMM 定義了一套在多執行緒讀寫共享資料時(成員變數、陣列)時,對資料的可見性、有序


性、和原子性的規則和保障

1.1 原子性

  • 原子性在學習執行緒時講過,下面來個例子簡單回顧一下:
  • 問題提出,兩個執行緒對初始值為 0 的靜態變數一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?
public class Demo8 {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 50000; j++) {
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 50000; j++) {
                i--;
            }
        });
        t1.start();
        t2.start();
        // join 主執行緒等待 t1 t2 執行緒執行完在去執行下面的操作
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
  • 答案是不一定

1.2 問題分析

  • 以上的結果可能是正數、負數、零。為什麼呢?因為 Java 中對靜態變數的自增,自減並不是原子操作
  • 例如對於 i++ 而言(i 為靜態變數),實際會產生如下的 JVM 位元組碼指令:
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
iadd // 加法
putstatic i // 將修改後的值存入靜態變數i
  • 而對應 i-- 也是類似:
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
isub // 減法
putstatic i // 將修改後的值存入靜態變數i
  • 而 Java 的記憶體模型如下,完成靜態變數的自增,自減需要在主存和執行緒記憶體中進行資料交換
  • 如果是單執行緒以上 8 行程式碼是順序執行(不會交錯)沒有問題:
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=1
iconst_1 // 執行緒1-準備常量1
isub // 執行緒1-自減 執行緒內i=0
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=0
  • 但多執行緒下這 8 行程式碼可能交錯執行(為什麼會交錯?思考一下):出現負數的情況:
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1
  • 出現正數的情況
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1

1.3 解決方法

  • synchronized (同步關鍵字)
  • 語法
synchronized( 物件 ) {
  要作為原子操作程式碼
}
  • 用 synchronized 解決併發問題:
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
如何理解呢:你可以把 obj 想象成一個房間,執行緒 t1,t2 想象成兩個人。
當執行緒 t1 執行到 synchronized(obj) 時就好比 t1 進入了這個房間,並反手鎖住了門,在門內執行
count++ 程式碼。
這時候如果 t2 也執行到了 synchronized(obj) 時,它發現門被鎖住了,只能在門外等待。
當 t1 執行完 synchronized{} 塊內的程式碼,這時候才會解開門上的鎖,從 obj 房間出來。t2 執行緒這時才
可以進入 obj 房間,反鎖住門,執行它的 count-- 程式碼。

注意:上例中 t1 和 t2 執行緒必須用 synchronized 鎖住同一個 obj 物件,如果 t1 鎖住的是 m1 對
象,t2 鎖住的是 m2 物件,就好比兩個人分別進入了兩個不同的房間,沒法起到同步的效果。

2. 可見性

2.1 退不出的迴圈

  • 先來看一個現象,main 執行緒對 run 變數的修改對於 t 執行緒不可見,導致了 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不會如預想的停下來
}

為什麼呢?分析一下:

  • 1. 初始狀態, t 執行緒剛開始從主記憶體讀取了 run 的值到工作記憶體
  • 2. 因為 t 執行緒要頻繁從主記憶體中讀取 run 的值,JIT 編譯器會將 run 的值快取至自己工作記憶體中的快取記憶體中,
    減少對主存中 run 的訪問,提高效率
  • 3. 1 秒之後,main 執行緒修改了 run 的值,並同步至主存,而 t 是從自己工作記憶體中的快取記憶體中讀
    取這個變數的值,結果永遠是舊值

2.2 解決方法

  • volatile(易變關鍵字)
  • 它可以用來修飾成員變數和靜態成員變數,他可以避免執行緒從自己的工作快取中查詢變數的值,必須到
    主存中獲取它的值,執行緒操作 volatile 變數都是直接操作主存

2.3 可見性

前面例子體現的實際就是可見性,它保證的是在多個執行緒之間,一個執行緒對 volatile 變數的修改對另一
個執行緒可見, 不能保證原子性,僅用在一個寫執行緒,多個讀執行緒的情況:
上例從位元組碼理解是這樣的

getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
putstatic run // 執行緒 main 修改 run 為 false, 僅此一次
getstatic run // 執行緒 t 獲取 run false

比較一下之前我們將執行緒安全時舉的例子:兩個執行緒一個 i++ 一個 i-- ,只能保證看到最新值,不能解
決指令交錯

// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1

注意
synchronized 語句塊既可以保證程式碼塊的原子性,也同時保證程式碼塊內變數的可見性。但缺點是
synchronized是屬於重量級操作,效能相對更低
如果在前面示例的死迴圈中加入 System.out.println() 會發現即使不加 volatile 修飾符,執行緒 t 也
能正確看到對 run 變數的修改了,想一想為什麼?