天天看點

JMM Java記憶體模型的三個特性以及實作1 原子性(Atomicity)2 可見性(Visibility)3 有序性(Ordering)4 總結

詳細介紹了Java 記憶體模型的原子性、可見性和有序性這 3個特性的含義以及解決辦法。

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

文章目錄

  • 1 原子性(Atomicity)
    • 1.1 Java記憶體模型的實作
    • 1.2 32位虛拟機long型變量多線程實驗
  • 2 可見性(Visibility)
    • 2.1 Java記憶體模型的實作
  • 3 有序性(Ordering)
    • 3.1 Java記憶體模型的實作
  • 4 總結

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學習部落格!

繼續閱讀