一、JMM的定義
1.什麼是JMM
《Java虛拟機規範》中曾試圖定義一種“Java記憶體模型”(Java Memory Model簡稱JMM)來屏蔽各種硬體和作業系統的記憶體通路差異,以實作讓Java程式在各種平台下都能達到一緻的記憶體通路效果。Java記憶體模型是一種抽象的概念,并不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變量(包括執行個體字段,靜态字段和構成數組對象的元素)的通路方式。JMM是圍繞原子性,有序性、可見性展開。
2.主記憶體與工作記憶體
Java記憶體模型的主要目的是定義程式中各種變量的通路規則,即關注在虛拟機中把變量值存儲到記憶體和從記憶體中取出變量值這樣的底層細節。此處的變量(Variables)與Java程式設計中所說的變量有所差別,它包括了執行個體字段、靜态字段和構成數組對象的元素,但是不包括局部變量與方法參數,因為後者是線程私有的,不會被共享,自然就不會存在競争問題。
Java記憶體模型規定了所有的變量都存儲在主記憶體(Main Memory)中(此處的主記憶體與介紹實體硬體時提到的主記憶體名字一樣,兩者也可以類比,但實體上它僅是虛拟機記憶體的一部分)。JVM運作程式的實體是線程,而每個線程建立時JVM都會為其建立一個工作記憶體(Working Memory,可與前面講的處理器高速緩存類比),用于存儲線程私有的資料,線程的工作記憶體中儲存了被該線程使用的變量的主記憶體副本,線程對變量的所有操作(讀取、指派等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的資料。不同的線程之間也無法直接通路對方工作記憶體中的變量,線程間變量值的傳遞均需要通過主記憶體來完成,線程、主記憶體、工作記憶體三者的互動關系如下圖。
3.JMM記憶體模型與JVM記憶體結構
這裡所講的主記憶體、工作記憶體與Java記憶體結構中的Java堆、棧、方法區等并不是同一個層次的對記憶體的劃分,這兩者基本上是沒有任何關系的。如果兩者一定要勉強對應起來,那麼從變量、主記憶體、工作記憶體的定義來看,主記憶體主要對應于Java堆中的對象執行個體資料部分,而工作記憶體則對應于虛拟機棧中的部分區域。從更基礎的層次上說,主記憶體直接對應于實體硬體的記憶體,而為了擷取更好的運作速度,虛拟機(或者是硬體、作業系統本身的優化措施)可能會讓工作記憶體優先存儲于寄存器和高速緩存中,因為程式運作時主要通路的是工作記憶體。
4.Java記憶體模型與硬體記憶體架構的關系
通過對前面的硬體記憶體架構、Java記憶體模型以及Java多線程的實作原理的了解,我們應該已經意識到,多線程的執行最終都會映射到硬體處理器上進行執行,但Java記憶體模型和硬體記憶體架構并不完全一緻。對于硬體記憶體來說隻有寄存器、緩存記憶體、主記憶體的概念,并沒有工作記憶體(線程私有資料區域)和主記憶體(堆記憶體)之分,也就是說Java記憶體模型對記憶體的劃分對硬體記憶體并沒有任何影響,因為JMM隻是一種抽象的概念,是一組規則,并不實際存在,不管是工作記憶體的資料還是主記憶體的資料,對于計算機硬體來說都會存儲在計算機主記憶體中,當然也有可能存儲到CPU緩存或者寄存器中,是以總體上來說,Java記憶體模型和計算機硬體記憶體架構是一個互相交叉的關系,是一種抽象概念劃分與真實實體硬體的交叉。(注意對于Java記憶體區域劃分也是同樣的道理)
二、JMM-記憶體間互動的八種操作
關于主記憶體與工作記憶體之間具體的互動協定,即一個變量如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體這一類的實作細節,Java記憶體模型中定義了以下8種操作來完成。Java虛拟機實作時必須保證下面提及的每一種操作都是原子的、不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平台上允許有例外)。
- lock(鎖定):作用于主記憶體的變量,它把一個變量辨別為一條線程獨占的狀态。
- unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
- read(讀取):作用于主記憶體的變量,它把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用。
- load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中。
- use(使用):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳遞給執行引擎,每當虛拟機遇到一個需要使用變量的值的位元組碼指令時将會執行這個操作。
- assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收的值賦給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。
- store(存儲):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳送到主記憶體中,以便随後的write操作使用。
- write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中。
如果要把一個變量從主記憶體拷貝到工作記憶體,那就要按順序執行read和load操作,如果要把變量從工作記憶體同步回主記憶體,就要按順序執行store和write操作。注意,Java記憶體模型隻要求上述兩個操作必須按順序執行,但不要求是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的,如對主記憶體中的變量a、b進行通路時,一種可能出現的順序是read a、read b、load b、load a。
除此之外,Java記憶體模型還規定了在執行上述8種基本操作時必須滿足如下規則:
- 不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主記憶體讀取了但工作記憶體不接受,或者工作記憶體發起回寫了但主記憶體不接受的情況出現。
- 不允許一個線程丢棄它最近的assign操作,即變量在工作記憶體中改變了之後必須把該變化同步回主記憶體。
- 不允許一個線程無原因地(沒有發生過任何assign操作)把資料從線程的工作記憶體同步回主記憶體中。
- 一個新的變量隻能在主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use、store操作之前,必須先執行assign和load操作。
- 一個變量在同一個時刻隻允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,隻有執行相同次數的unlock操作,變量才會被解鎖。
- 如果對一個變量執行lock操作,那将會清空工作記憶體中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作以初始化變量的值。
- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執行unlock操作之前,必須先把此變量同步回主記憶體中(執行store、write操作)。
由Java記憶體模型來直接保證的原子性變量操作包括read、load、assign、use、store和write這六個,我們大緻可以認為,基本資料類型的通路、讀寫都是具備原子性的(例外就是long和double的非原子性協定)。
如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java記憶體模型還提供了lock和unlock操作來滿足這種需求,盡管虛拟機未把lock和unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隐式地使用這兩個操作。這兩個位元組碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,是以在synchronized塊之間的操作也具備原子性。
參考:深入了解java虛拟機(第三版)