BAT經典面試題,深入理解Java內存模型JMM

Java 內存模型

Java 內存模型(JMM)是一種抽象的概念,並不真實存在,它描述了一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。試圖屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致的內存訪問效果。

注意JMM與JVM內存區域劃分的區別:

  • JMM描述的是一組規則,圍繞原子性、有序性和可見性展開;

  • 相似點:存在共享區域和私有區域

主內存與工作內存

處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。

加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,需要一些協議來解決這個問題。

FJfmy2F.png!web

所有的變量都 存儲在主內存中,每個線程還有自己的工作內存 ,工作內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。

線程只能直接操作工作內存中的變量,不同線程之間的變量值傳遞需要通過主內存來完成。

Jbuiiyn.png!web

數據存儲類型以及操作方式

  • 方法中的基本類型本地變量將直接存儲在工作內存的棧幀結構中;

  • 引用類型的本地變量:引用存儲在工作內存,實際存儲在主內存;

  • 成員變量、靜態變量、類信息均會被存儲在主內存中;

  • 主內存共享的方式是線程各拷貝一份數據到工作內存中,操作完成後就刷新到主內存中。

內存間交互操作

Java 內存模型定義了 8 個操作來完成主內存和工作內存的交互操作。

2A36niU.png!web

  • read:把一個變量的值從主內存傳輸到工作內存中

  • load:在 read 之後執行,把 read 得到的值放入工作內存的變量副本中

  • use:把工作內存中一個變量的值傳遞給執行引擎

  • assign:把一個從執行引擎接收到的值賦給工作內存的變量

  • store:把工作內存的一個變量的值傳送到主內存中

  • write:在 store 之後執行,把 store 得到的值放入主內存的變量中

  • lock:作用於主內存的變量

  • unlock

指令重排序的條件

  • 在單線程環境下不能改變程序的運行結果;

  • 存在數據依賴關係的不允許重排序;

  • 無法通過Happens-before原則推到出來的,才能進行指令的重排序。

內存模型三大特性

1. 原子性

Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執行 assign 賦值操作,這個操作就是原子性的。但是 Java 內存模型允許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操作劃分爲兩次 32 位的操作來進行,即 load、store、read 和 write 操作可以不具備原子性。

有一個錯誤認識就是,int 等原子性的類型在多線程環境中不會出現線程安全問題。前面的線程不安全示例代碼中,cnt 屬於 int 類型變量,1000 個線程對它進行自增操作之後,得到的值爲 997 而不是 1000。

爲了方便討論,將內存間的交互操作簡化爲 3 個:load、assign、store。

下圖演示了兩個線程同時對 cnt 進行操作,load、assign、store 這一系列操作整體上看不具備原子性,那麼在 T1 修改 cnt 並且還沒有將修改後的值寫入主內存,T2 依然可以讀入舊值。可以看出,這兩個線程雖然執行了兩次自增運算,但是主內存中 cnt 的值最後爲 1 而不是 2。因此對 int 類型讀寫操作滿足原子性只是說明 load、assign、store 這些單個操作具備原子性。

bQ7RRvA.png!web

AtomicInteger 能保證多個線程修改的原子性。

y6BjYn3.png!web

使用 AtomicInteger 重寫之前線程不安全的代碼之後得到以下線程安全實現:

public class AtomicExample {    private AtomicInteger cnt = new AtomicInteger();    public void add() {
        cnt.incrementAndGet();
    }    public int get() {        return cnt.get();
    }
}複製代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改這條語句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}複製代碼
1000複製代碼

除了使用原子類之外,也可以使用 synchronized 互斥鎖來保證操作的原子性。它對應的內存間交互操作爲:lock 和 unlock,在虛擬機實現上對應的字節碼指令爲 monitorenter 和 monitorexit。

public class AtomicSynchronizedExample {    private int cnt = 0;    public synchronized void add() {
        cnt++;
    }    public synchronized int get() {        return cnt;
    }
}複製代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }    countDownLatch.await();    executorService.shutdown();    System.out.println(example.get());
}複製代碼
1000複製代碼

2. 可見性

可見性指當一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。Java 內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。JMM 內部的實現通常是依賴於所謂的 內存屏障 ,通過 禁止某些重排序 的方式,提供內存 可見性保證 ,也就是實現了 各種 happen-before 規則 。與此同時,更多複雜度在於,需要儘量確保各種編譯器、各種體系結構的處理器,都能夠提供一致的行爲。

主要有有三種實現可見性的方式:

  • volatile,會 強制 將該變量自己和當時其他變量的狀態都 刷出緩存 。

  • synchronized,對一個變量執行 unlock 操作之前,必須把變量值同步回主內存。

  • final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,並且沒有發生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那麼其它線程就能看見 final 字段的值。

對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,因爲 volatile 並不能保證操作的原子性。

3. 有序性

有序性是指:在本線程內觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因爲發生了指令重排序。在 Java 內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

volatile 關鍵字通過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障之前。

也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼。

先行發生原則(Happen-Before)

JSR-133內存模型使用先行發生原則在Java內存模型中保證多線程操作 可見性 的機制,也是對早期語言規範中含糊的可見性概念的一個精確定義。上面提到了可以用 volatile 和 synchronized 來保證有序性。除此之外,JVM 還規定了先行發生原則,讓一個操作 無需控制 就能先於另一個操作完成。

由於 指令重排序 的存在,兩個操作之間有happen-before關係, 並不意味着前一個操作必須要在後一個操作之前執行。 僅僅要求前一個操作的執行結果對於後一個操作是可見的,並且前一個操作 按順序 排在第二個操作之前。

1. 單一線程原則(程序員順序規則)

Single Thread rule

在一個線程內,在程序前面的操作先行發生於後面的操作。

Vr2Iry6.png!web

2. 管程鎖定規則(監視器鎖規則)

Monitor Lock Rule

一個 unlock(解鎖) 操作 先行發生於 後面對同一個鎖的 lock(加鎖) 操作。

m2ENria.png!web

3. volatile 變量規則

Volatile Variable Rule

對一個 volatile 變量的 寫操作 先行發生於後面對這個變量的 讀操作 。

riQjaqE.png!web

4. 線程啓動規則

Thread Start Rule

Thread 對象的 start() 方法調用先行發生於此線程的每一個動作。

IjERZ3n.png!web

5. 線程加入規則

Thread Join Rule

Thread 對象的結束先行發生於 join() 方法返回。

memq2qN.png!web

6. 線程中斷規則

Thread Interruption Rule

對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷髮生。

7. 對象終結規則

Finalizer Rule

一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。

8. 傳遞性

Transitivity

如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那麼操作 A 先行發生於操作 C。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章