1. 程式人生 > 實用技巧 >Java記憶體模型--JMM

Java記憶體模型--JMM

Java記憶體模型 (JMM)和JVM執行時記憶體的區別

JVM執行時記憶體

Java執行時記憶體模型,描述了Java程式程式碼在執行時,一次執行單個語句或者表示式時(即通過單個執行緒執行時)不同型別的變數、引用、物件、類等等的一些資訊的儲存規範

Java記憶體模型

描述了多個執行緒執行時的語義規範,比如多個執行緒修改了共享記憶體的值時,應該讀取到哪個值得規則。

下邊我們來看一個多執行緒讀取的一個問題程式碼

/**
 * 提供一個產生多執行緒可見性的原因程式碼
 * 理論上在子執行緒在主執行緒將flag改為false之後應該打印出i的值,但實際執行中沒有
 */
public class MultiThreadProblem {

    public static boolean flag = true;
    //public volatile static boolean flag = true; //這樣就可以保證flag變數的可見性

    public static void main(String[] args) throws InterruptedException {
        //開啟一個子執行緒來根據flag的值進行i++操作
        new Thread(()->{
            int i = 0;
            System.out.println(Thread.currentThread().getName() + "正在執行," + "flag=" + flag);

            while (flag){
                i++;
            }
            System.out.println("flag=false,i=" + i);
        }).start();

        //主執行緒睡眠3s後改變flag的值
        Thread.sleep(3000L);
        flag =false;
        System.out.println("主執行緒更改了flag的值為:" + flag);

    }
}

上邊問題引發的原因是,兩個執行緒都從記憶體共享區的方法區複製了一份flag變數的值true放到各自的虛擬機器棧中,雖然主執行緒將更改了flag=false,並且通過也將更改的值覆蓋了方法區的值,但是子執行緒在執行while語句的時候,CPU因為while的時間操作較長而將while語句進行了指令重排。CPU指令重排在單執行緒中是沒有問題的,但是在多執行緒中操作一個共享變數,指令重排就可能會對結果產生影響。
因為Java語言是介於指令碼語言和編譯語言之間,當直譯器讀到while語句的時候,JIT編譯器遇到while迴圈和反覆呼叫的情況,就會將當前的程式碼進行指令重排。然後這個指令重排,對於多執行緒來說,如果其他執行緒對於共享變數有更改的話,就可能產生可見性問題,就像上邊程式碼中將flag改為false後,子執行緒不會感知的到。

使用了volatile關鍵字之後,就會在編譯的時候告訴JVM有關該變數的操作不進行指令重排,一旦有執行緒更改了變數之後,就會將該變數寫入記憶體中。

這些關於多執行緒中共享記憶體(或者堆記憶體)中的共享變數衝突問題,就是JMM約束的範圍,它描述執行緒間操作的語義規範。

JMM規定有以下幾個:

  1. 對於同步的規則定義參考圖片

  2. happens-before先行發生原則,參考圖片

  3. 被final定義的物件,對於所有執行緒都會看到正確的構造版本。通常被static final修飾的欄位不能被修改。然而System.in/System.out/System.err被static final修飾,卻可以被修改,這是個遺留問題。必須通過set方法改變,我們將這些欄位稱為防寫,以區別於普通final欄位。

  4. Word Tearing(字分裂)位元組處理,參考圖片

  5. double 和long的特殊處理
    這兩個都是64位的,它的單次寫操作都是分兩次來進行的,每次操作其中32位的時候,可能導致第一次寫入後,讀取的值是髒資料,第二次寫完成後,才能讀到正確資料。所以建議用volatile來修飾double和long才行。