Java記憶體模型簡介
併發處理的廣泛應用代替了摩爾定律成為計算機效能發展的源動力,也是人類“壓榨”計算機運算能力的最有力武器。
本文將介紹處理器的記憶體模型,JMM即Java的記憶體模型,和執行緒的安全性問題。
處理器的記憶體模型
由於計算機的儲存裝置與處理器的運算速度有幾個數量級差距,所以現代計算機系統都會加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體(Cache)來作為記憶體和處理器之間的緩衝,將運算需要使用的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步到記憶體之中。如下圖所示:

快取記憶體從下到上越接近 CPU 速度越快,同時容量也越小。現在大部分的處理器都有二級或者三級快取,從下到上依次為 L3 cache,L2 cache, L1 cache。快取又可以分為指令快取和資料快取,指令快取用來快取程式的程式碼,資料快取用來快取程式的資料。
L1 Cache,一級快取,本地 core 的快取,分成 32K 的資料快取 和 32k 指令快取 ,訪問 L1 需要3cycles,耗時大約 1ns;
L2 Cache,二級快取,本地 core 的快取,大小為 256K,訪問 L2 需要 12cycles,耗時大約 3ns;
L3 Cache,三級快取,在同插槽的所有 core 共享 L3 快取,分為多個 2M 的段,訪問 L3 需要 38cycles,耗時大約 12ns;

CPU的術語定義
快取一致性問題
基於快取記憶體的儲存互動很好地解決了處理器與記憶體的速度矛盾,但是也為計算機系統帶來更高的複雜度,因為它引入了一個新的問題:快取一致性(Cache Coherence)。
CPU-0 讀取主存的資料,快取到 CPU-0 的快取記憶體中,CPU-1 也做了同樣的事情,而 CPU-1 把 count 的值修改成了 2,並且同步到 CPU-1 的快取記憶體,但是這個修改以後的值並沒有寫入到主存中,CPU-0 訪問該位元組,由於快取沒有更新,所以仍然是之前的值,就會導致資料不一致的問題。為了解決這個問題,CPU 生產廠商提供了相應的解決方案:
匯流排鎖
當一個 CPU 對其快取中的資料進行操作的時候,往匯流排中傳送一個 Lock 訊號。其他處理器的請求將會被阻塞,那麼該處理器可以獨佔共享記憶體。匯流排鎖相當於把 CPU 和記憶體之間的通訊鎖住了,所以這種方式會導致 CPU 的效能下降,所以 P6 系列以後的處理器,出現了另外一種方式,就是快取鎖。
快取鎖
如果快取在處理器快取行中的記憶體區域在 LOCK 操作期間被鎖定,當它執行鎖操作回寫記憶體時,處理不在總線上宣告 LOCK 訊號,而是修改內部的快取地址,然後通過快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改被兩個以上處理器快取的記憶體區域的資料,當其他處理器回寫已經被鎖定的快取行的資料時會導致該快取行無效。所以快取鎖會產生兩個作用:
1、將當前處理器快取行的資料寫回到系統記憶體。
2、這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。
快取一致性協議
處理器上有一套完整的協議,來保證 Cache 的一致性。比較經典的是MESI 協議,它的方法是在 CPU 快取中儲存一個標記位,這個標記為有四種狀態:
Ø M(Modified) 修改快取
Ø I(Invalid) 失效快取
Ø E(Exclusive) 獨佔快取
Ø S(Shared) 共享快取
每個 Core 的 Cache 控制器不僅知道自己的讀寫操作,也監聽其它 Cache 的讀寫操作,嗅探(snooping)協議。CPU 的讀取會遵循幾個原則:
1、如果快取的狀態是 I,那麼就從記憶體中讀取,否則直接從快取讀取
2、如果快取處於 M 或者 E 的 CPU 嗅探到其他 CPU 有讀的操作,就把自己的快取寫入到記憶體,並把自己的狀態設定為 S
Java記憶體模型
Java虛擬機器規範中試圖定義一種Java記憶體模型(Java Memory Model,JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。
定義Java記憶體模型並非一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓Java的併發記憶體訪問操作不會產生歧義;但是,也必須足夠得寬鬆,使得虛擬機器的實現有足夠的自由空間去利用硬體的各種特性(暫存器、快取記憶體和指令集)來獲取更好的執行速度。
在 JMM 抽象模型中,分為主記憶體、工作記憶體。主記憶體是所有執行緒共享的,工作記憶體是每個執行緒獨有的。執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,不能直接讀寫主記憶體中的變數。並且不同的執行緒之間無法訪問對方工作記憶體中的變數,執行緒間的變數值的傳遞都需要通過主記憶體來完成,他們三者的互動關係如下:

關於主記憶體和工作記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,Java記憶體模型定義上述8種操作來完成,虛擬機器實現時必須保證上面提及的每一種操作都是原子的、不可再分的。
執行緒的安全性問題
原子性:是指一個操作是不可中斷的,即使是在多執行緒環境下,一個操作一旦開始就不會被其他執行緒影響。
可見性:是指當一個執行緒修改了某個共享變數的值,其他執行緒是否能夠馬上得知這個修改的值。
有序性:是指程式執行的順序按照程式碼的先後順序執行。
在介紹有序性時,有必要了解一下指令重排序。在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。重排序分3種類型。
1)編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
2)指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
3)記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。