CPU快取與Java記憶體模型
CPU多級快取
區域性性原理:
- 時間區域性性:如果某個資料被訪問,那麼在不久的將來它很可能再次被訪問;
- 空間區域性性:如果某個資料被訪問,那麼它相鄰的資料很快也可能被訪問。
快取一致性(MESI)
定義了四種cache life的四種狀態:
狀態 | 描述 |
---|---|
M(Modified) | 這行資料有效,資料被修改了,和記憶體中的資料不一致,資料只存在於本Cache中。 |
E(Exclusive) | 這行資料有效,資料和記憶體中的資料一致,資料只存在於本Cache中。 |
S(Shared) | 這行資料有效,資料和記憶體中的資料一致,資料存在於很多Cache中。 |
I(Invalid) | 這行資料無效。 |
CPU多級快取——亂序執行優化
硬體記憶體架構:
通常情況下,當一個CPU需要讀取主存時,它會將主存的部分讀到CPU快取中。它甚至可能將快取中的部分內容讀到它的內部暫存器中,然後在暫存器中執行操作。當CPU需要將結果寫回到主存中去時,它會將內部暫存器的值重新整理到快取中,然後在某個時間點將值重新整理回主存。
一些問題:(多執行緒環境下尤其)
快取一致性問題: 當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的快取資料不一致的情況,如果真的發生這種情況,需要各個處理器訪問快取時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI等。
指令重排序問題:
Java記憶體模型(JMM)
JVM記憶體模型指的是JVM的記憶體分割槽;而Java記憶體模式是一種虛擬機器規範。
JVM模型
JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的: 規定了一個執行緒如何和何時可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。
Java記憶體模型(不僅僅是JVM記憶體分割槽):
- 某個本地變數可能是原始變數,始終“呆在”執行緒棧上
- 某個本地變數可能是一個物件的引用,物件本身依然放在堆(heap)上,但這個引用放在本地變數上。
- 物件中如果包含方法,這些方法可能包含本地變數,本地變數依然放線上程棧上,即使物件和包含的方法放在堆(heap)上。
- 物件的成員變數是跟隨物件一起放在heap上的,不論這個物件是原始變數或者引用。
- 存放在heap上的物件可被所持有的這個物件的執行緒訪問,同時該執行緒可以訪問該物件的成員變數;如果兩個執行緒同時呼叫該類的方法,訪問其成員變數,那麼這兩個執行緒都擁有了這個成員變數的私有拷貝。
Java記憶體模型和硬體記憶體架構之間的橋接
JVM與硬體記憶體架構存在差異;
硬體記憶體架構沒有區分執行緒棧和堆;
對於硬體而言,所有的執行緒棧和堆都在主記憶體裡,部分執行緒的棧和堆會存在於CPU暫存器與cache中。
抽象角度看,JMM模型中定義了執行緒與主記憶體的關係:
- 執行緒之間的共享變數存放在主記憶體中;
- 每個執行緒都有一個私有的本地記憶體(JMM的抽象概念),裡面存放了該執行緒讀/寫共享變數的拷貝副本;
- 低層次角度看,主記憶體就是硬體記憶體,為了獲取更高的執行速度,虛擬機器及硬體系統會將工作記憶體優先儲存與暫存器和快取記憶體中;
- JMM中執行緒的工作記憶體,是CPU的暫存器和快取記憶體的抽象描述;而JVM的靜態儲存模型(JVM記憶體模型)只是一種對記憶體的物理劃分而已,它只侷限於記憶體。
JMM模型下的執行緒間通訊:
執行緒間通訊必須要經過主記憶體。
Java記憶體模型定義了以下八種操作來完成:
- lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
- unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
- load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
- assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:
- 如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順尋地執行read和load操作, 如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
- 不允許read和load、store和write操作之一單獨出現
- 不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
- 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
- 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。
- 一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。lock和unlock必須成對出現
- 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值
- 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
- 對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。
Java記憶體模型解決的問題
多執行緒讀同步與可見性
可見性(共享物件可見性):執行緒對共享變數修改的可見性。當一個執行緒修改了共享變數的值,其他執行緒能夠立刻得知這個修改;
A、執行緒快取導致的可見性問題:
跑在左邊CPU的執行緒拷貝這個共享物件到它的CPU快取中,然後將count變數的值修改為2。這個修改對跑在右邊CPU上的其它執行緒是不可見的,因為修改後的count的值還沒有被重新整理回主存中去。
解決這種可見性問題:
- volatile關鍵字:volatile關鍵字可以保證直接從主存中讀取一個變數,如果這個變數被修改後,總是會被寫回到主存中去。普通變數與volatile變數的區別是:volatile的特殊規則保證了新值能立即同步到主記憶體,以及每個執行緒在每次使用volatile變數前都立即從主記憶體重新整理。因此我們可以說volatile保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。
- synchorized關鍵字:同步塊的可見性是由:
“如果對一個變數執行lock操作,將會清空工作記憶體中變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值”
“對一個變數執行unlock操作之前,必須先把此變數的值同步到主記憶體中(執行store 和write操作) - final關鍵字:final關鍵字的可見性是指,被final修飾的欄位在構造器中一旦被初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他執行緒有可能通過這個引用訪問到“初始化了一半”的物件),那麼在其他執行緒就能看見final欄位的值(無須同步)
B、重排序導致的可見性問題:
Java程式中天然的有序性可以總結為一句話:
如果在本地執行緒內觀察,所有操作都是有序的(“執行緒內表現為序列”(Within-Thread As-If-Serial Semantics));
如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的(“指令重排序”現象和“執行緒工作記憶體與主記憶體同步延遲”現象)。
Java語言提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性:
volatile關鍵字本身就包含了禁止指令重排序的語義
synchronized則是由“一個變數在同一個時刻只允許一條執行緒對其進行lock操作”這條規則獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能序列地進入
作者:Echo_IX