Java多執行緒記憶體模型

Java學習錄發表於2019-03-22


JMM的基本概念

Java作為平臺無關性語言,JLS(Java語言規範)定義了一個統一的記憶體管理模型JMM(Java Memory Model)。JMM規定了jvm記憶體分為主記憶體和工作記憶體 ,主記憶體存放程式中所有的類例項、靜態資料等變數,是多個執行緒共享的,而工作記憶體存放的是該執行緒從主記憶體中拷貝過來的變數以及訪問方法所取得的區域性變數,是每個執行緒私有的其他執行緒不能訪問。每個執行緒對變數的操作都是以先從主記憶體將其拷貝到工作記憶體再對其進行操作的方式進行,多個執行緒之間不能直接互相傳遞資料通訊,只能通過共享變數來進行。
2
從上圖來看,執行緒1與執行緒2之間如要通訊的話,必須要經歷下面2個步驟:

  1. 首先,執行緒1把本地工作記憶體中更新過的共享變數重新整理到主記憶體中去。
  2. 然後,執行緒2到主記憶體中去讀取執行緒1之前已更新過的共享變數。
    典型的高併發引起的問題就存在由於執行緒讀取到的資料還沒有從另外的執行緒重新整理到主記憶體中而引起的資料不一致問題。


主記憶體與工作記憶體的資料互動

JLS一共定義了8種操作來完成主記憶體與執行緒工作記憶體的資料互動:

lock:把主記憶體變數標識為一條執行緒獨佔,此時不允許其他執行緒對此變數進行讀寫
unlock:解鎖一個主記憶體變數
read:把一個主記憶體變數值讀入到執行緒的工作記憶體
load:把read到變數值儲存到執行緒工作記憶體中作為變數副本
use:執行緒執行期間,把工作記憶體中的變數值傳給位元組碼執行引擎
assign:位元組碼執行引擎把運算結果傳回工作記憶體,賦值給工作記憶體中的結果變數
store:把工作記憶體中的變數值傳送到主記憶體
write:把store傳送進來的變數值寫入主記憶體的變數中
複製程式碼

使用標準的操作再來重現一下上方的2個執行緒之間的互動流程則是這樣的:
執行緒1從主記憶體read一個值為0的變數x到工作記憶體
使用load把變數x儲存到工作記憶體作為變數副本
將變數副本x使用use傳遞給位元組碼執行引擎進行x++操作
位元組碼執行引擎操作完畢後使用assign將結果賦值給變數副本
使用store把變數副本傳送到主記憶體
使用write把store傳送的資料寫到主記憶體
執行緒2從主記憶體read到x,然後load–>use–>assign–>store–>write

另外使用這8種操作也有一些規則:
read 和 load必須以組合的方式出現,不允許一個變數從主記憶體讀取了但工作記憶體不接受情況出現
store和write必須以組合的方式出現,不允許從工作記憶體發起了儲存操作但主記憶體不接受的情況出現
工作記憶體的變數如果沒有經過 assign 操作,不允許將此變數同步到主記憶體中
在 use 操作之前,必須經過 load 操作
在 store 操作之前,必須經過 assign 操作
unlock 操作只能作用於被 lock 操作鎖定的變數
一個變數被執行了多少次 lock 操作就要執行多少次 unlock 才能解鎖
一個變數只能在同一時刻被一條執行緒進行 lock 操作
執行 lock 操作後,工作記憶體的變數的值會被清空,需要重新執行 load 或 assign 操作初始化變數的值
對一個變數執行 unlock 操作之前,必須先把此變數同步回主記憶體中


多執行緒中的原子性、可見性、有序性

原子性:關於原子性的定義可以參考我的上篇部落格《淺談資料庫事務》。在JLS中保證原子性的操作包括read、load、assign、use、store和write。基本資料型別(除了long 和double)操作都具有原子性。
如果需要更大範圍的原子性操作的時候,可以使用lock和unlock操作來完成這種需求。
可見性:是指當一個執行緒修改了共享變數的值,其他執行緒是否能夠立即得知這個修改。
由上方JMM的概念得知,執行緒運算元據是在工作記憶體的,當多個執行緒操作同一個資料的時候很容易讀取到還沒有被write到主記憶體變數的值。
Java是如何保證可見性的:volatile、synchronized、final關鍵字
有序性:在併發時,程式的執行可能會出現亂序。給人的直觀感覺就是:寫在前面的程式碼,會在後面執行。有序性問題的原因是因為程式在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致。關於指令重排會在下方講。


指令重排

1234複製程式碼
int a=1;int b=2;int c=3;int d=4;複製程式碼

你能說出上方這段程式碼的執行順序麼?其實我們可能理所當然的以為它會從上往下順序執行。事實上,在實際執行時,為了優化指令的執行順序等,程式碼指令可能並不是嚴格按照程式碼語句順序執行的。上方的程式碼執行順序可能完全反過來,這個就是指令重排。
不過呢,指令重排也不是可以隨意重排的,它需要遵守一定的規則:
程式順序規則:一個執行緒內保證語義的正確性。
鎖規則:解鎖肯定先於隨後的加鎖前。
volatile規則:對一個volatile的寫,先於volatile的讀。
傳遞性:如果A 先於 B,且B 先於 C,那麼A 肯定先於 C。
start()規則:執行緒的start()操作先於執行緒的其他操作。
join()規則:執行緒的所有操作先於執行緒的關閉。
程式中斷規則:執行緒的中斷先於被中斷後執行的程式碼。
物件finalize規則:一個物件的初始化完成先於finalize()方法。


volatile關鍵字

volatile關鍵字旨在告訴虛擬機器在這個地方要注意不能隨意的進行指令重排,而虛擬機器看到一個變數被volatile修飾以後就會採用一些特殊的手段來保證變數的可見性。不過要注意的是volatile關鍵字不能保證原子性。

Java多執行緒記憶體模型


相關文章