JVM 技術內幕——Java 記憶體模型
併發處理的廣泛應用是使得Amdahl定律替代摩爾定律成為計算機效能發展原動力的根本原因。
說明 | |
---|---|
摩爾定律 | 用於描述處理器電晶體數量與執行效率之間的發展關係。 |
Amdahl定律 | 通過系統中並行化與序列化的比重來描述多處理器系統能獲得的運算加速能力。 |
由於計算機的主記憶體與CPU的運算速度有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近CPU運算速度的快取記憶體(L1 Cache、L2 Cache、L3 Cache)來作為主記憶體與CPU之間的緩衝。將運算需要使用到的資料複製到快取記憶體中,讓運算能快速進行,當運算結束後再從快取記憶體同步回主記憶體之中,這樣CPU就無須等待緩慢的記憶體讀寫了。
CPU <--> 快取記憶體
CPU <--> 快取記憶體 <--> 快取一致性協議 <--> 主記憶體
CPU <--> 快取記憶體
除了增加快取記憶體之外,為了使得CPU內部的運算單元能儘量被充分利用,CPU可能會對輸入程式碼進行亂序執行優化,CPU會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程式中各個語句計算的先後順序與輸入程式碼中的順序一致,因此,如果存在一個計算任務依賴另外一個計算任務的中間結果,那麼其順序性並不能靠程式碼的先後順序來保證。與CPU的亂序執行優化類似,JVM 的 JIT編譯器中也有類似的指令重排序優化。
JMM(Java記憶體模型)遮蔽掉了各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。
1.主記憶體與工作記憶體
JMM 規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體(類比前面說到的快取記憶體),執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成,執行緒、主記憶體、工作記憶體三者互動關係如下:
Java執行緒 <--> 工作記憶體 Java執行緒 <--> 工作記憶體 <--> Save和Load操作 <--> 主記憶體 Java執行緒 <--> 工作記憶體
主記憶體主要對應於Java堆中的物件例項資料部分,而工作記憶體對應於JVM棧中的部分割槽域。從更低層次來說,主記憶體就直接對應於物理硬體的記憶體,而為了獲取更好的執行速度,JVM可能會讓工作記憶體優先儲存於暫存器和快取記憶體中,因為程式執行時主要讀寫訪問的是工作記憶體。
2.記憶體間互動操作
關於一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體這類實現細節,JMM 定義了以下8種操作來完成:
說明 | |
---|---|
lock(鎖定) | 作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。 |
unlock(解鎖) | 作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。 |
read(讀取) | 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。 |
load(載入) | 作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。 |
use(使用) | 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當JVM遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。 |
assign(賦值) | 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當JVM遇到一個給變數賦值的位元組碼指令時執行這個操作。 |
store(儲存) | 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給主記憶體中,以便隨後的write操作使用。 |
write(寫入) | 作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。 |
這些操作都是原子的、不可再分的(對於double和long型別的變數來說,read、load、store、write操作在某些平臺上允許有例外,在目前商用JVM中不會出現)。
3.對於volatile型變數的特殊規則
關鍵詞volatile可以說是JVM提供的最輕量級的同步機制,當一個變數定義為volatile之後,它將具備兩種特性:
1、保證此變數對所有執行緒的可見性,可見性是指當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的,而普通變數不能做到這一點(普通變數的值線上程間傳遞均需要通過主記憶體來完成)。
注意:volatile變數在各個執行緒中是一致的,但是基於volatile變數的運算在併發下不一定是安全的。volatile變數在各個執行緒的工作記憶體中不存在不一致問題(在各個執行緒的工作記憶體中,volatile變數也可能存在不一致的情況,但由於每次使用之前都要先重新整理,執行引擎看不到不一致的情況,因此可以認為不存在一致性問題),但是Java裡面的運算並非原子操作,導致volatile變數的運算在併發下一樣是不安全的。
由於volatile變數只能保證可見性,在不符合以下兩種規則的運算場景中,我們仍然要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性:
運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值;
變數不需要與其他的狀態變數共同參與不變約束。
2、使用volatile變數第二個語義是禁止指令重排序優化,普通的變數僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一致。