JVM-內存模型

Java併發是基於共享內存模型實現的。學習並深刻地理解__Java內存模型__,有助於開發人員瞭解Java的線程間通訊機制原理,從而實現安全且高效的多線程功能。java

處理器內存模型

計算機在執行程序時,每條指令都是在__CPU__中執行的,而執行指令過程當中,勢必涉及到對主存中數據的讀取和寫入。因爲__CPU__的處理速度相比對內存數據的訪問速度快不少,若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在__CPU__裏面就有了高速緩存。編程

640.png | center | 607x294

然而引入高速緩存帶來方便的同時,也帶來了緩存一致性的問題。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自緩存數據不一致的問題。解決方法是緩存一致性協議(如Intel 的MESI協議)。緩存

MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。當CPU寫數據時,若是發現操做的變量是共享變量,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態。所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。安全

除了增長高速緩存以外,爲了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致,所以,若是存在一個計算任務依賴另一個計算任務的中間結果,那麼其順序性並不能靠代碼的前後順序來保證。多線程

Java內存模型

Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規範的,屏蔽了各類硬件和操做系統的訪問差別的,保證了Java程序在各類平臺下對內存的訪問都能保證效果一致的機制及規範架構

爲了方便理解Java內存模型,咱們能夠抽象地認爲,全部變量都存儲在主內存中(Main Memory),每一個線程都擁有一個私有的工做內存(Working Memory),保存了該線程已訪問的變量副本。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。併發

640-2.png | center | 641x377

假設__線程A__要向__線程B__發消息,__線程A__須要先在本身的工做內存中更新變量,再將變量同步到主內存中,隨後__線程B__再去主內存中讀取A更新過的變量。於是能夠看出,JMM經過控制主內存與每一個線程的本地內存之間的交互,提供內存可見性保證。app

內存交互操做

Java內存模型定義的8個操做指令來進行內存之間的交互,以下:函數

  • read 讀取主內存的值,並傳輸至工做內存。
  • load 將read的變量值存放到工做內存。
  • use 將工做內存的變量值,傳遞給執行引擎。
  • assign 執行引擎對變量進行賦值。
  • store 工做內存將變量傳輸到主內存。
  • write 主內存將工做內存傳遞過來的變量進行存儲。
  • lock 用做主內存變量,它把一個變量在內存裏標識爲一個線程獨佔狀態。
  • unlock 用做主內存變量,它對被鎖定的變量進行解鎖。

工做內存和主內存間的指令操做交互,以下圖所示:性能

image | left

指令規則

  • read 和 load、store和write必須成對出現
  • assign操做,工做內存變量改變後必須刷回主內存
  • 同一時間只能運行一個線程對變量進行lock,當前線程lock可重入,unlock次數必須等於lock的次數,該變量才能解鎖。
  • 對一個變量lock後,會清空該線程工做內存變量的值,從新執行load或者assign操做初始化工做內存中變量的值。
  • unlock前,必須將變量同步到主內存(store/write操做)。

重排序

在沒有正確同步的狀況下,即便要推斷最簡單的併發程序的行爲也很困難。代碼以下:

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
    Thread one = new Thread(new Runnable() {
        public void run() {
            a = 1;
            x = b;
        }
    });

    Thread other = new Thread(new Runnable() {
        public void run() {
            b = 1;
            y = a;
        }
    });
    one.start();
    other.start();
    one.join();other.join();
    System.out.println(「(」 + x + 「,」 + y + 「)」);
}
複製代碼

很容易想象PossibleReordering的輸出結果是(1,0)或(0,1)或(1:1)的,__但奇怪的是__還能夠輸出(0,0)。

因爲每一個線程中的各個操做之間不存在數據依賴性,所以這些操做能夠亂序執行。下圖給出了一種可能由重排序致使的交替執行方式,在這種狀況中會輸出(0,0)。

image | left

Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序,如圖所示:

源代碼到最終指令過程.png | left

在執行程序時,爲了提升性能,編譯器和處理器會對指令作重排序。重排序分3種類型:

  • 編譯器優化重排序。編譯器在不改變單線程程序語義(as-if-serial semantics)的前提下,可從新安排語句的執行順序。
  • 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  • 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

先行發生原則(happens-before)

爲了提升執行性能,JMM容許編譯器和處理器對指令進行重排序。可是Java語言保證了操做間具備必定的有序性,歸納起來就是先行發生原則(happens-before)。也就是說,若是兩個操做的關係沒法被happens-before原則推導,則沒法保證它們的順序性,有可能發生重排序。happens-before原則包括:

  • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做
  • 鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做。
  • volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做
  • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做
  • 線程終結規則:線程中的全部操做都先行發生於對此線程的終止檢測
  • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 對象終結規則:一個對象的初始化完成先行發生於它的finalize()方法的開始
  • 傳遞規則:若是操做A先行發生於操做B,操做B先行發生於操做C,則有A先行發生於操做C。

實際上,這些規則是由編譯器重排序規則和處理器內存屏障插入策略來實現的。

內存屏障

內存屏障是一條CPU指令,用於控制特定條件下的重排序和內存可見性問題。即任何指令都不能與內存屏障指令重排序。

  • LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
  • StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。

處理器對重排序的支持

image | left

從上面能夠看到不一樣的處理器架構對重排序的支持也是不同(其它處理器架構暫不羅列),因此不一樣的平臺JMM的內存屏障施加也略有不一樣,具體來講,好比 X86 對Load1Load2不支持重排序,那麼你就沒有必要施加 LoadLoad屏障。

volatile的內存語義

volatile關鍵字用來保證數據可見性,防止指令重排的效果。包括JUC裏AQS Lock的底層實現也是基於volatitle來實現。

  • volatile寫的內存語義:當寫一個volatile變量的時候,JMM會把該線程對應的本地內存變量值刷新到主內存。
  • volatile讀的內存語義:當讀一個volatile變量的時候,JMM會把線程本次內存置爲無效。線程接下來將從主內存中讀取共享變量(也就是從新從主內存獲取值,更新運行內存中的本地變量)。

final的內存語義

final修飾的稱做域,對於final域,編譯器和處理器要遵照兩個重排序規則

  • 寫規則:在構造函數內對一個final域的寫入,與隨後把這個被構造的對象的引用賦值給一個引用變量,這兩個操做不可重排序。

    JMM禁止編譯器把final域的寫重排序到構造函數以外, 編譯器會在final域寫入的後面插入StoreStore屏障,該規則能夠保證在對象引用爲任意線程可見以前,對象的final域已經被正確初始化,而普通域沒法保障。

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

    在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做。編譯器會在讀final域操做的前面插入一個LoadLoad屏障。

類庫Happens-Before

因爲Happens-Before的排序功能很強,所以有時候能夠「藉助」現有機制的可見性屬性。這須要將Happens-Before的程序順序規則與其餘某個順序規則(一般是監視器鎖規則或者volatile變量規則)結合起來,從而對某個未被鎖保護的變量的訪問操做進行排序。由類庫擔保的其餘happens-before排序包括:

  • 將一個元素放入線程安全容器happens-before於另外一個線程從容器中獲取元素。
  • 執行CountDownLatch中的倒計時happens-before於線程從閉鎖(latch)的await中返回。
  • 釋放一個許可證Semaphorehappens-before於從同一Semaphore裏得到一個許可。
  • Future表現的任務所發生的動做happens-before於另外一個線程成功地從Future.get中返回。
  • Executor提交一個Runnable或Callable happens-before於開始執行任務。
  • 一個線程到達CyclicBarrier或Exchanger happens-before於相同關卡(barrier)或Exchange點中的其餘線程被釋放。若是CyclicBarrier使用一個關卡(barrier)動做,到達關卡happens-before於關卡動做,依照次序,關卡動做happens-before於線程從關卡中釋放。

參考資料

相關文章
相關標籤/搜索