Java 內存模型

Java 內存模型(Java Memory Model, JMM) 定義了 JVM 操做內存的行爲模式。java

內存模型

JVM 將進程內存分爲線程棧區(Thread Stack)和堆區(Heap)。安全

JVM 上運行的每一個線程都有本身的線程棧, 且只能訪問本身的線程棧。併發

棧的每一幀是一層方法調用,即調用棧。棧幀中保存了方法的局部變量、下一條指令的指針等運行時信息。app

對於 int, boolean 等 built-in 類型變量自己保存在棧中;對於各類類對象,對象自己保存在堆中,若其引用做爲局部變量則會保存在棧中。函數

對象中的域不管是 built-in 類型仍是類對象都會保存在堆中,built-in 類型的域會在對象中存儲域自己,而類對象只保存其引用。就是說 Java 中全部類對象都是以引用的形式進行訪問的。性能

JMM 定義了堆與線程棧之間6種交互行爲: load, save, read, write, assign 和 use。這些交互行爲具備原子性,且相互依賴。ui

咱們能夠簡單的認爲線程在在修改堆區某對象的值時會先將其拷貝到線程工做內存中修改完成後再將其寫入回主內存。this

指令重排序與 happens-before

爲了充分發揮多核 CPU 的性能, Java 規範規定 JVM 線程中程序的執行結果只要等同於嚴格順序執行的結果,JVM 能夠對指令進行從新排序或並行執行,即 as-is-serial 語義。線程

咱們來看一個經典的示例:指針

public class Main {
    
    private int a = 1;
    
    private int b = 2;

    public void foo() {
        a = 3;
        b = 4;
    }
}

在線程 A 執行 main.foo 方法時線程 B 試圖訪問 main.amain.b 可能產生 4 種結果:

  • a = 1, b = 2: 均未改變
  • a = 3, b = 2: a 已改變, b 未改變
  • a = 3, b = 4: a、b 均已改變
  • a = 1, b = 4: a 未改變, b 已改變

根據 as-is-serial 語義, a = 3b = 4 兩條語句不管誰先執行均不影響結果,所以 JVM 能夠先執行任意語句。

a = 1 語句並非原子性的,包含將對象從堆拷貝線程工做內存,修改,寫回堆操做。 JVM 不保證兩條語句的內存操做是有序的, 可能 a 先修改,但 b 先寫回堆區。

綜上兩點,JVM 不保證其它線程看到 a、b 修改的順序。

JMM 爲了解決不一樣線程訪問對象狀態的順序一致性定義了 happens-before 規則。即想要保證執行動做 B 時能夠看到動做 A 的結果(不論 A、B 是否在同一個線程中), 動做 A 必須 happens-before 於動做 B。

  • 程序次序規則: 線程中每一個動做A 都happens-before 於該線程中的每個動做B。那麼在程序中,全部的動做B都能出如今A以後。(即要求同一個線程中知足 as-is-serial 語義)
  • 監視器鎖法則: 對一個監視器鎖的解鎖 happens-before 於每個後續對同一監視器鎖的加鎖。(包括 synchronized 或 ReentrantLock 等)
  • volatile 變量法則: 對 volatile 域的寫入操做 happens-before 於每個後續對同一域的讀操做。 即 volatile 域的寫入對其它線程當即可見。原子性變量一樣擁有 volatile 語義。
  • 線程啓動法則: Thread.start 的調用會 happens-before 於線程中其它全部動做
  • 線程終止法則: 線程中的任何動做都 happens-before 於其餘線程檢測到這個線程已終結(包括從 Thread.join 方法調用中成功返回; thread.isAlive() == false)
  • 線程中斷法則: 一個線程調用另外一個線程的 interrupt 方法 happens-before 於被中斷線程發現中斷(包括拋出InterruptedException、thread.isInterrupted() == true)
  • 對象終結法則: 對象構造器返回 happens-before 於對象終結過程開始

happens-before 是一個標準的偏序關係,具備傳遞性。

volatile 與 synchronized

synchronized 關鍵字會阻止其它線程得到對象的監視器鎖,被保護的代碼塊沒法被其它線程訪問也就沒法併發執行。

synchronized 也會建立一個內存屏障,保證全部操做結果會被直接寫入主存中。也就是說,synchronized 會保證操做的原子性和可見性。

舉例來講,在多個線程同時嘗試更新同一個計數器時, 更新操做須要進行 讀取-修改-寫入 操做, 若沒法保證更新操做i++的原子性則可能出現異常執行順序: 線程A讀取舊值i=0 -> 線程B讀取舊值i=0 -> 線程A在線程工做內存中修改i, 並寫入結果 i=1 -> 線程B寫入結果i=1。 最終致使兩個線程調用i++最終i只加1的狀況。

上文已經提到, volatile 關鍵字保證變量可見性,即對 volatile 域修改對於其它線程當即可見。

volatile 關鍵字能夠達到保證部分 built-in 類型(如 int、 short、 byte)操做原子性的效果, 但對於 double、 long 類型沒法保證原子性。即便對於 int 類型 i++ 這類須要讀取-修改-寫入操做的語句也沒法保證原子性。

所以, 不要使用 volatile 關鍵字來保證操做的原子性。請使用AtomicInteger等原子數據類或synchronized等鎖機制來保證。

volatile 同時會禁止指令重排序。咱們經過經典的單例模式雙重檢查鎖實現來分析 volatile 禁止指令重排序的意義:

public class Singleton {
    
    volatile private static Singleton instance;
    
    private Singleton (){}

    public static Singleton getInstance() {
      if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
      }
      return instance;
    }
}

分析若 instance 域不使用 volatile 關鍵字修飾時可能出現的情況。

instance = new Singleton() 能夠分爲三步:

  1. 爲 instance 引用分配內存
  2. 調用 Singleton 的構造函數進行初始化
  3. 將 instance 引用指向分配的內存空間

在不由止指令重排序的狀況下可能出現1-2-3或1-3-2兩種執行順序。 若執行順序爲 1-3-2, 在線程A執行3後 instance 引用指向還沒有初始化的對象。

此時線程B調用 getInstance 方法, 判斷instance != null 因而訪問了未初始化的對象形成錯誤。所以,須要使用 volatile 關鍵字禁止指令重排序。

final

使用不可變對象是保證線程安全最簡單可靠的辦法(笑

Java 對 final 域的重排序有以下約束:

  • 在構造函數內對一個final域的寫入, 與將一個引用指向被構造對象的操做之間不能重排序。

  • 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

就是說,JMM 對 final 域的保證可理解爲它只會在構造函數返回以前插入一個存儲屏障,保證構造函數內對 final 域的賦值在構造函數返回以前寫到主存。

所以,爲了保證對 final 域訪問的安全性須要防止在構造函數返回前將被構造對象的引用暴露出去。

public class Escape {
       
  private final int a; 

  public Escape (int a, List<Escape> list) {
    this.a = a;
    list.add(this);
  }
}

其它線程可能經過構造器中的 list 列表在構造器返回前得到 this 指針, 此時對final域的訪問是不安全的。

相關文章
相關標籤/搜索