1. 程式人生 > >JVM | Java內存模型

JVM | Java內存模型

副本 而且 nag 速度 賦值 obj 發出 傳輸 clas

前言

“天下武功,唯快不破”,火雲邪神告訴了你體術中追求的境界;相對論也告訴大家當你的移動速度逐漸超過光速甚至再快更快,你就很容易去到詩和遠方,遊火星,逛土星,浪跡天涯;當單核計算機從出現到一代代地提升性能,運算力也在更快更強。甚至就是奧運會都追求“更快、更高、更強”,似乎“快”對人們有著與生俱來的誘惑。那麽“快節奏和從前慢一生只夠愛一個人”,你又有著怎樣的思考呢,抱歉~這裏暫不討論。其實啊,人們不斷壓榨計算工具的運算力和老板不停壓榨員工的體力一樣也都是有快感的。既然是壓榨,總有一天可能幾近榨不出油水,咋辦?
技術分享圖片
這不,單核CPU的主頻不可能無限制的增長,Intel老板給跪了。再想提升性能,於是CPU進入多核時代,多個處理器協同工作。什麽,協同工作?小學自習課最能咋呼的是你,中學最不服管的也是你,其實很多時候不是追求的一加一大於二,而是一加一不小於一,人越多越亂,事越多越煩,一個道理,增加CPU數量可不是簡單的一加一,變量越多,帶來的不確定性也就越多,天賦異稟的人更適合做管理者,當然具備足夠完善和周密的算法的操作系統才能協同好計算機。

多任務處理在現代計算機操作系統中幾乎是一項必備的功能。許多情況下,讓計算機同時去做幾件事情,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的存儲和通信子系統速度的差距太大。計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代計算機都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存來作為內存與處理器之間的緩沖,這種解決思想呢就是緩沖技術。
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是也為計算機系統帶來了更高的復雜度,引入了新的問題:緩存一致性。在多處理器系統中,每個處理器都有自己的高速緩存,而又共享同一主內存。

處理器內存概念模型

技術分享圖片

Java內存概念模型

技術分享圖片

編譯器和處理器

  • 有著相同目標,在不改變程序執行結果的前提下,盡可能提高並行度。

  • 處理器不會改變存在數據依賴關系的兩個操作的執行順序。

  • 處理器保證單線程程序的重排序不改變執行結果。

  • 越是追求性能的處理器,內存模型設計得越弱,束縛少,盡可能多的優化來提高性能。

  • 編譯器不會改變存在數據依賴關系的兩個操作的執行順序。

  • 編譯器保證單線程程序的重排序不改變執行結果。

重排序

  • 處理器重排序

除了增加高速緩存之外,為了使得處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執行優化,處理器會在計算之後將亂序執行的結構重組。

  • 數據依賴性

如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,那麽這兩個操作之間存在數據依賴性。
1)寫這個變量後,再讀這個變量;
2)寫這個變量後,再寫這個變量;
3)讀這個變量後,再寫這個變量;
上面3種情況,只要重排兩個操作的執行順序,執行結果就會被改變。

  • as-if-serial

語義是:不管編譯器和處理器為了提高並行度怎麽重排序,單線程程序的執行結果不能被改變。

  • happens-before

happens-before要求禁止的重排序分為兩類:
1)會改變程序執行結果的重排序。
JMM處理策略要求編譯器和處理器必須禁止這種重排序。
2)不會改變程序執行結果的重排序。
JMM處理策略允許編譯器和處理器這種重排序。

規則定義:
1)程序順序規則:一個線程中的每個操作,happens-before該線程的任意後續操作。
2)監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那麽A happens-before C。
5)start()規則:如果線程A執行操作ThreadB.start()啟動線程B,那麽A線程的ThreadB.start()操作happens-before於線程B中的任意操作。
6)join()規則:如果線程A執行操作Thread.join()並成功返回,那麽線程B中的任意操作happens-before於線程A從Thread.join()操作成功返回。

  • 小結

1)as-if-serial語義保證單線程內程序的執行結果不被改變;happens-before關系保證正確同步的多線程程序的執行結果不被改變。
2)as-if-serial語義創造單線程程序幻境:單線程程序是按程序的順序來執行的;happens-before關系創造多線程程序幻境:正確同步的多線程程序是按happens-before關指定的順序來執行的。

Java內存模型

  • Java內存模型規範對數據競爭的定義:
    在一個線程中寫一個變量,在另一個線程讀同一個變量,而且寫和讀沒有通過同步來排序。

  • JMM允許編譯器和處理器只要不改變程序執行結果,包括單線程程序和正確同步的多線程程序,怎麽優化都行。

  • 常見的處理器內存模型比JMM要弱,Java編譯器在生成字節碼時,會在執行指令序列的適當位置插入內存屏障來限制處理器的重排序。各種處理器內存模型的強弱不同,JMM在不同處理器中插入的內存屏障的數量和種類也不相同。

  • 內存間交互操作:

1)lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占的狀態。
2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,解鎖後的變量才可以被其他線程鎖定。
3)read(讀取):作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load操作使用。
4)load(載入):作用於工作內存的變量,把read操作從主內存中得到的變量值放入工作內存的變量副本中。
5)use(使用):作用於工作內存的變量,把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到的變量的值的字節碼指令時將會執行這個操作。
6)assign(賦值):作用於工作內存的變量,把一個從執行引擎收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
7)store(存儲):作用於工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。
8)write(寫入):作用於主內存的變量,把store操作沖工作內存中得到的變量的值放入主內存的變量中。

  • 執行上述8種基本操作時必須滿足的規則:

1)不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
2)不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。
3)不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
4)一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化的變量,換句話說,對一個變量實施use、store操作之前,必須先執行過了assign和load操作。
5)一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量才會被解鎖。
6)如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
7)如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
8)對一個變量執行unlock操作之前,必須先把此變量同步回主內存中。

鎖的內存語義

語義:

  • 當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。
  • 當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。

線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

實現:
見AQS等。

volatile內存語義

語義:

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
  • 當讀一個volatile變量是,JMM會把該線程對應的本地內存置為無效。

線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所做修改的)消息。
線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。
線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A通過主內存向線程B發送消息。

JMM實現:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。
  • 在每個volatile讀操作的前面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的後面插入一個LoadStore屏障。

表現:

  • 可見性:對一個volatile變量的讀,總是能看到任意線程對這個volatile變量最後的寫入。
  • 原子性:對任意單個volatile變量的讀寫具有原子性,但類似volatile++這種符合操作不具備原子性。

final內存語義

對於final域,編譯器和處理器要遵守兩個重排序規則:

  • 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
  • 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。

編譯器final語義具體實現:

  • 寫final域的重排序規則會要求編譯器在final域的寫之後,構造函數return之前插入一個StoreStore屏障。
  • 讀final域的重排序規則要求編譯器在讀final域的操作前面插入一個LoadLoad屏障。
public class FinalExample {
    int i;
    final int j;
    static FinalExample obj;
    public FinalExample(){
        i = 1;
        j = 2;
        // final域 StoreStore屏障 在這裏
        // 確保構造函數return前 final域 j=2 完成

    }
    public static void write(){
        obj = new FinalExample();
    }
    public static void reader(){
        FinalExample object = obj;
        int a = object.i;
        // final域 LoadLoad屏障 在這裏
        // 確保初次讀對象包含的final域前 讀對象引用完成
        int b = object.j;
    }
}

總結

  • 處理器和編譯器都期望在不改變程序執行結果的前提下,盡可能提高並行度。
  • 處理器和編譯器都可能會對輸入代碼進行亂序執行優化,充分利用運算單元。
  • 處理器和編譯器都不會改變存在數據依賴關系的兩個操作的執行順序。
  • 處理器和編譯器都能保證單線程程序的重排序不改變執行結果。
  • 處理器內存模型都比JMM要弱,更偏向性能考慮。
  • JMM屏蔽了跨平臺處理器,對不同處理器進行不同程度的禁止指令重排序,來盡可能保障正確語義。
  • 看透as-if-serial語義和happens-before關系,能幫助程序員深入理解並發編程,編輯高效、健壯代碼。

參考文獻《深入理解Java 虛擬機》、《Java並發編程的藝術》

JVM | Java內存模型