Java 記憶體模型的作用
《Java虛擬機規范》中曾試圖定義一種“Java記憶體模型”(Java Memory Model,JMM)來屏蔽各種硬體和作業系統的記憶體訪問差異, 以實作讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果,
在此之前, 主流程式語言(如 C 和 C++ 等) 直接使用物理硬體和作業系統的記憶體模型, 因此, 由于不同平臺上記憶體模型的差異, 有可能導致程式在一套平臺上并發完全正常, 而在另外一套平臺上并發訪問卻經常出錯, 所以在某些場景下必須針對不同的平臺來撰寫程式,
主記憶體 & 作業記憶體
Java記憶體模型的主要目的是定義程式中各種變數的訪問規則,即關注在虛擬機中把變數值存盤到記憶體和從記憶體中取出變數值這樣的底層細節,
Java記憶體模型規定了所有的變數都存盤在主記憶體(Main Memory) 中(此處的主記憶體與介紹物理硬體時提到的主記憶體名字一樣, 兩者也可以類比, 但物理上它僅是虛擬機記憶體的一部分) ,每條執行緒還有自己的作業記憶體(Working Memory, 可與前面講的處理器高速快取類比) , 執行緒的作業記憶體中保存了被該執行緒使用的變數的主記憶體副本,執行緒對變數的所有操作(讀取、 賦值等) 都必須在作業記憶體中進行, 而不能直接讀寫主記憶體中的資料,不同的執行緒之間也無法直接訪問對方作業記憶體中的變數, 執行緒間變數值的傳遞均需要通過主記憶體來完成,
此處的變數(Variables) 與 Java 編程中所說的變數有所區別,
此處的變數包括了實體欄位、靜態欄位和構成陣列物件的元素,但是不包括區域變數與方法引數,因為區域變數與方法引數是執行緒私有的,不會被共享,自然就不會存在競爭問題,
執行緒、主記憶體、作業記憶體三者的互動關系如圖所示,

記憶體間互動操作
關于主記憶體與作業記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到作業記憶體、如何從作業記憶體同步回主記憶體這一類的實作細節, Java記憶體模型中定義了以下 8 種記憶體訪問操作來完成,
- lock(鎖定):作用于主記憶體的變數,它把一個變數標識為一條執行緒獨占的狀態,
- unlock(解鎖):作用于主記憶體的變數,它把一個處于鎖定狀態的變數釋放出來,釋放后的變數才可以被其他執行緒鎖定,
- read(讀取):作用于主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的作業記憶體中, 以便隨后的 load 操作使用,
- load(載入):作用于作業記憶體的變數,它把 read 操作從主記憶體中得到的變數值放入作業記憶體的變數副本中,
- use(使用):作用于作業記憶體的變數,它把一個變數的值從作業記憶體傳輸到執行引擎, 每當虛擬機遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作,
- assign(賦值):作用于作業記憶體的變數,它把一個從執行引擎接收的值賦給作業記憶體的變數副本,每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作,
- store(存盤):作用于作業記憶體的變數,它把一個變數的值從作業記憶體傳輸到主記憶體中, 以便隨后的 write 操作使用,
- write(寫入):作用于主記憶體的變數,它把 store 操作從作業記憶體中得到的變數的值放入主記憶體的變數中,
Java虛擬機實作時必須保證上面提及的每一種記憶體訪問操作都是原子的、不可再分的(對于 double 和 long 型別的變數來說,load、 store、 read 和 write 操作在某些平臺上允許有例外, 這個問題會專門討論),

如果要把一個變數從主記憶體拷貝到作業記憶體, 那就要按順序執行 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 操作之前, 必須先執行 load、assign 操作,
- 一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作, 但 lock 操作可以被同一條執行緒重復執行多次,多次執行 lock 后, 只有執行相同次數的 unlock 操作, 變數才會被解鎖,
- 如果對一個變數執行 lock 操作,那將會清空作業記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行 load 或 assign 操作以初始化變數的值,
- 如果一個變數事先沒有被 lock 操作鎖定, 那就不允許對它執行 unlock 操作, 也不允許去 unlock 一個被其他執行緒鎖定的變數,
- 對一個變數執行 unlock 操作之前, 必須先把此變數同步回主記憶體中(執行 store 和 write 操作) ,
這 8 種記憶體訪問操作以及上述規則限定, 再加上稍后會介紹的專門針對 volatile 的一些特殊規定, 就已經能準確地描述出 Java 程式中哪些記憶體訪問操作在并發下是安全的,
針對 volatile 的特殊規則
Java 記憶體模型為 volatile 專門定義了一些特殊的訪問規則,
假定 T 表示一個執行緒, V 和 W 分別表示兩個 volatile 型變數, 那么在進行 read、 load、 use、 assign、 store 和 write 操作時需要滿足如下規則:
只有當執行緒 T 對變數 V 執行的前一個動作是 load 的時候, 執行緒 T 才能對變數 V 執行 use 動作; 并且,只有當執行緒 T 對變數 V 執行的后一個動作是 use 的時候, 執行緒 T 才能對變數 V 執行 load 動作,執行緒 T 對變數 V 的 use 動作可以認為是和執行緒 T 對變數 V 的 load、 read 動作相關聯的, 必須連續且一起出現,
這條規則要求在作業記憶體中, 每次使用 V 前都必須先從主記憶體重繪最新的值, 用于保證能看見其他執行緒對變數 V 所做的修改,
只有當執行緒 T 對變數 V 執行的前一個動作是 assign 的時候, 執行緒 T 才能對變數 V 執行 store 動作; 并且, 只有當執行緒 T 對變數 V 執行的后一個動作是 store 的時候, 執行緒 T 才能對變數 V 執行 assign 動作, 執行緒 T 對變數 V 的 assign 動作可以認為是和執行緒 T 對變數 V 的 store、 write 動作相關聯的, 必須連續且一起出現,
這條規則要求在作業記憶體中, 每次修改 V 后都必須立刻同步回主記憶體中, 用于保證其他執行緒可以看到自己對變數 V 所做的修改,
假定動作 A 是執行緒 T 對變數 V 實施的 use 或 assign 動作, 假定動作 F 是和動作 A 相關聯的 load 或 store 動作, 假定動作 P 是和動作 F 相應的對變數 V 的 read 或 write 動作;
與此類似, 假定動作 B 是執行緒 T 對變數 W 實施的 use 或 assign 動作, 假定動作 G 是和動作 B 相關聯的 load 或 store 動作, 假定動作 Q 是和動作 G 相應的對變數 W 的 read 或 write 動作,
如果 A 先于 B, 那么 P 先于 Q,
這條規則要求 volatile 修飾的變數不會被指令重排序優化, 從而保證代碼的執行順序與程式的順序相同,
先行發生原則
先行發生原則(Happens-Before)是Java記憶體模型定義的一個等效判斷原則,依賴這個原則, 我們可以通過幾條簡單規則一攬子解決并發環境下兩個操作之間是否可能存在沖突的所有問題, 而不需要陷入Java記憶體模型苦澀難懂的定義之中,
“先行發生”原則指的是什么, 先行發生是Java記憶體模型中定義的兩項操作之間的偏序關系, 比如說操作 A 先行發生于操作 B, 其實就是說在發生操作 B 之前, 操作 A 產生的影響能被操作 B 觀察到, “影響”包括修改了記憶體中共享變數的值、 發送了訊息、 呼叫了方法等,
下面是Java記憶體模型下一些“天然的”先行發生關系, 這些先行發生關系無須任何同步器協助就已經存在,可以在編碼中直接使用,如果兩個操作之間的關系不在此列,并且無法從下列規則推匯出來, 則它們就沒有順序性保障,虛擬機可以對它們隨意地進行重排序,
- 程式次序規則(Program Order Rule) : 在一個執行緒內, 按照控制流順序, 書寫在前面的操作先行發生于書寫在后面的操作,注意, 這里說的是控制流順序而不是程式代碼順序, 因為要考慮分支、 回圈等結構,
- 管程鎖定規則(Monitor Lock Rule) : 一個 unlock 操作先行發生于后面對同一個鎖的 lock 操作, 這里必須強調的是“同一個鎖”, 而“后面”是指時間上的先后,
- volatile 變數規則(Volatile Variable Rule) : 對一個 volatile 變數的寫操作先行發生于后面對這個變數的讀操作,這里的“后面”同樣是指時間上的先后,
- 執行緒啟動規則(Thread Start Rule) : Thread 物件的 start() 方法先行發生于此執行緒的每一個動作,
- 執行緒終止規則(Thread Termination Rule) : 執行緒中的所有操作都先行發生于對此執行緒的終止檢測,我們可以通過 Thread::join() 方法是否結束、 Thread::isAlive() 的回傳值等手段檢測執行緒是否已經終止執行,
- 執行緒中斷規則(Thread Interruption Rule) : 對執行緒 interrupt() 方法的呼叫先行發生于被中斷執行緒的代碼檢測到中斷事件的發生, 可以通過 Thread::interrupted() 方法檢測到是否有中斷發生,
- 物件終結規則(Finalizer Rule) : 一個物件的初始化完成(建構式執行結束) 先行發生于它的 finalize() 方法的開始,
- 傳遞性(Transitivity):如果操作 A 先行發生于操作 B, 操作 B 先行發生于操作 C, 那就可以得出操作 A 先行發生于操作 C 的結論,
Java 語言無須任何同步手段保障就能成立的先行發生規則有且只有上面這些,
“時間上的先后順序”與“先行發生”之間有什么不同?一個操作“時間上的先發生”不代表這個操作會是 “先行發生”,那如果一個操作“先行發生”, 是否就能推匯出這個操作必定是“時間上的先發生”呢? 很遺憾, 這個推論也是不成立的,
時間先后順序與先行發生原則之間基本沒有因果關系,所以我們衡量并發安全問題的時候不要受時間順序的干擾, 一切必須以先行發生原則為準,
參考資料
《深入理解Java虛擬機》第 12 章:Java 記憶體模型與執行緒 12.3 Java 記憶體模型
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/17297939.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549479.html
標籤:Java
下一篇:【演算法資料結構專題】「延時佇列演算法」史上手把手教你針對層級時間輪(TimingWheel)實作延時佇列的開發實戰落地(下)
