java內存模型

Java 內存模型試圖屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致的內存訪問效果。

主內存與工作內存

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

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

 

 

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

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

 

內存間交互操作

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

 

  • read:把一個變量的值從主內存傳輸到工作內存中
  • load:在 read 之後執行,把 read 得到的值放入工作內存的變量副本中
  • use:把工作內存中一個變量的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工作內存的變量
  • store:把工作內存的一個變量的值傳送到主內存中
  • write:在 store 之後執行,把 store 得到的值放入主內存的變量中
  • lock:作用於主內存的變量
  • unlock

內存模型三大特性

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 這些單個操作具備原子性。

 

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 內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。

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

  • volatile
  • synchronized,對一個變量執行 unlock 操作之前,必須把變量值同步回主內存。
  • final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,並且沒有發生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那麼其它線程就能看見 final 字段的值。

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

3. 有序性

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

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

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

先行發生原則

上面提到了可以用 volatile 和 synchronized 來保證有序性。除此之外,JVM 還規定了先行發生原則,讓一個操作無需控制就能先於另一個操作完成。

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。

發佈了16 篇原創文章 · 獲贊 2 · 訪問量 1737
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章