Java內存模型(JMM)的三個特性以及實現

  Java 內存模型是圍繞着在併發過程中如何處理原子性、可見性和有序性這 3個特性來建立的,歸根究底,是爲實現共享變量的在多個線程的工作內存的數據一致性。這三個特性(也可以說是問題),是人們抽象定義出來的,而這個抽象的底層問題就是前面提到的處理器優化問題、緩存一致性問題和指令重排問題等。

1 原子性(Atomicity)

  這個概念和數據庫事務中的原子性大概一致。表明此操作是不可分割的,不可中斷,要全部執行,要麼全部不執行。這個特性的重點在於不可中斷,如下代碼:

int a=0;  //1
int b=0;  //2
int c=0;  //3

  線程A執行上述代碼,從內存中讀取這三個變量的值,在讀取的過程中,此時線程B也讀取了某一個變量的值,此時雖然線程B的這個操作並不會對線程A的結果產生影響,但是線程A的原子性已經不存在了,在底層CPU執行的時候,就會涉及到切換線程A、B。並且,對A要進行中斷,所以線程A的原子性就被破壞了。理解這一點,也就會理解關鍵字volatile並不能保證原子性,保證原子性需要加鎖。
  在單例模式中,如果不是使用加鎖的方法,就會因爲沒有保證原子性,而使得對象會可能被創建多個;
  又如,在設計計數器時一般都先讀取當前值,然後+l,再更新。這個過程是讀改寫的過程,該過程不是天然原子性的,如果不能保證這個過程是原子性的,那麼就會出現線程安全問題。

1.1 Java內存模型的實現

  如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java 內存模型提供了 lock 和 unlock 操作來滿足這種需求,儘管虛擬機未把 lock 和 unlock 操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作,這兩個字節碼指令反映到 Java 代碼中就是synchronized 關鍵字,因此在 synchronized 塊/方法之間的操作也具備原子性。
  Java中的天然原子操作包括:
1)除long和double之外的基本類型的賦值操作(32位虛擬機會分兩次讀、寫long、double類型變量)
2)所有引用reference的賦值操作
3)java.concurrent.Atomic.* 包中所有類的一切操作。

1.2 32位虛擬機long型變量多線程實驗

  對於32位虛擬機來說,對long型數據的讀寫不是原子性的。對int型數據讀寫是原子性的。如下案例,使用32位虛擬機運行:

/**
 * 32位虛擬機下演示
 */
public class MultiThreadLong {
    public volatile static long t = 0;

    public static class ChangeT implements Runnable {
        private long to;
        public ChangeT(long to) {
            this.to = to;
        }
        @Override
        public void run() {
            while (true) {
                MultiThreadLong.t = to;     //賦值臨界區的t
                Thread.yield();            //讓出資源
            }
        }
    }
    public static class ReadT implements Runnable {
        @Override
        public void run() {
            while (true) {
                long tmp = MultiThreadLong.t;
                if (tmp != 111L && tmp != -999L && tmp != 333L && tmp != -444L) {
                    System.out.println(tmp);    //打印非正常值
                }
                Thread.yield();            //讓出資源
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new ChangeT(111L)).start();
        new Thread(new ChangeT(-999L)).start();
        new Thread(new ChangeT(333L)).start();
        new Thread(new ChangeT(-444L)).start();
        new Thread(new ReadT()).start();
        //在32位虛擬機下運行,將可能輸出:
        //-4294966963
        //4294966852
        //-4294966963
    }
}

  如果我給出這幾個數值的2進制(補碼)表示, 大家就會有更清晰的認識了:

+111=0000000000000000000000000000000000000000000000000000000001101111
-999=1111111111111111111111111111111111111111111111111111110000011001
+333=0000000000000000000000000000000000000000000000000000000101001101
-444=1111111111111111111111111111111111111111111111111111111001000100
+4294966852=0000000000000000000000000000000011111111111111111111111001000100
-4294967185=1111111111111111111111111111111100000000000000000000000001101111

  上面顯示了這幾個相關數字的補碼形式,也就是在計算機內的真實存儲內容。不難發現,這個奇怪的4294966852, 其實是111 或者333的前32位,與-444的後32位夾雜後的數字 。而-4294967185只是-999或者-444的前32位與111夾雜後的數字。換句話說,由於並行的關係,數字被寫亂了,或者讀的時候,讀串位了。

2 可見性(Visibility)

  可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。

2.1 Java內存模型的實現

  Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新,底層使用了內存屏障。而普通變量則不能保證心智能夠立即同步會主內存中。
  除了volatile之外,Java還有兩個關鍵字能實現可見性,即synchronized和final。synchronized的可見性是由“對一個變量執行lock(加鎖)操作之前,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中 重新獲取最新的值,對一個變量執行unlock(解鎖)操作之前,必須先把此變量同步回主內存中(執行store、write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就一定能看見final字段的值,且該值不可修改。

3 有序性(Ordering)

  先看重排序的概念:Java 內存模型允許編譯器和處理器對指令重排序以提高運行性能, 並且只會對不存在數據依賴性的指令重排序。在單線程下重排序可以保證最終執行的結果與程序順序執行的結果一致,但是在多線程下就會存在問題。 關於重排序的原理,在前面的文章中已經有過深入探討:硬件的效率與緩存一致性概述,大家可以看看這篇文章。

3.1 Java內存模型的實現

  有了重排序,並且重排序在多線程環境下可能出現問題,那麼自然有了有序性的概念。Java程序中天然的有序性可以總結爲一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行的語義”(Within-Thread As-If-Serial Semantics),後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象,如果不加額外的限制多線程下程序的有序性就不能得到保證。
  因此,Java語言額外提供了volatile和synchronized兩個關鍵字來保證多線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義(通過加入內存屏障指令),而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入,相當於單線程了。 這兩個區別就是,synchronized可以修飾一段代碼,或者一個方法。但是volatile只能修飾一個變量。

4 總結

  Java內存模型不會爲我們自動處理爲原子性、可見性和有序性這 3個特性(問題),但是均提供了相應的解決辦法。在開發過程中,需要開發人員自己根據場景選擇合適的手段去解決這些問題,常用手段包括synchronized、volatile、final、使用併發包、Threadlocal等方式。本文只是淺顯的介紹了這些方法,相當於一個在總結,具體這些方式的底層實現和使用方法,將在後面的文章中一一分析。

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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