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

Java 內存模型

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

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

  • JMM描述的是一組規則,圍繞原子性、有序性和可見性展開;
  • 類似點:存在共享區域和私有區域

主內存與工做內存

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

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

 

全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。緩存

線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。安全

 

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

  • 方法中的基本類型本地變量將直接存儲在工做內存的棧幀結構中;
  • 引用類型的本地變量:引用存儲在工做內存,實際存儲在主內存;
  • 成員變量、靜態變量、類信息均會被存儲在主內存中;
  • 主內存共享的方式是線程各拷貝一份數據到工做內存中,操做完成後就刷新到主內存中。

內存間交互操做

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

 

  • 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。app

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

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

 

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

 

使用 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

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

 

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

Monitor Lock Rule

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

 

3. volatile 變量規則

Volatile Variable Rule

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

 

4. 線程啓動規則

Thread Start Rule

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

 

5. 線程加入規則

Thread Join Rule

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

 

6. 線程中斷規則

Thread Interruption Rule

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

7. 對象終結規則

Finalizer Rule

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

8. 傳遞性

Transitivity

若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼操做 A 先行發生於操做 C。


免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q​​​​​​​
相關文章
相關標籤/搜索