注:本文參考自周志明老師的著作《深入理解Java虛擬機(第3版)》,相關電子書可以關注WX公眾號,回復 001 獲取,
1. Java記憶體模型
JMM概述:
Java 記憶體模型指的是 JMM,而不是運行時資料區哦~
-
Java 語言為了保證并發編程中可以滿足原子性、可見性及有序性,于是推出了一個概念就是 JMM 記憶體模型,
-
JMM 記憶體模型,目的是為了在多執行緒條件下,使用共享記憶體進行資料通信時,通過對多執行緒程式讀操作、寫操作行為規范約束,來盡量避免多次記憶體資料讀取不一致、編譯器對代碼指令重排序、處理器對代碼亂序執行等帶來的問題,
- JMM 記憶體模型解決并發問題主要采用兩種方式:
限制處理器優化和使用記憶體屏障, - JMM 記憶體模型將記憶體主要劃分為主記憶體和作業記憶體兩種,規定 所有的變數都存盤在主記憶體中,每條執行緒都擁有自己的作業記憶體,執行緒的作業記憶體中保存了該執行緒所需要用到的變數在主記憶體中的副本拷貝,執行緒對變數的所有操作都必須在作業記憶體中進行,而不能直接讀、寫主記憶體,
- 不同的執行緒之間也無法直接訪問對方作業記憶體中的變數,執行緒間變數的傳遞均需要執行緒自己的作業記憶體和主存之間進行資料互動,
如圖所示:
- JMM 記憶體模型解決并發問題主要采用兩種方式:

JMM 記憶體模型作業記憶體、主記憶體和 JVM 記憶體有什么關系?
JMM 記憶體模型中,作業記憶體和主記憶體其實跟JVM記憶體的劃分是在不同層次上進行的,是自己的一套抽象概念,大概可以理解為,主記憶體對應的是 Java 堆中的物件實體部分,而作業記憶體對應的則是堆疊中的部磁區域,
1.1 主記憶體與作業記憶體
Java記憶體模型規定了所有的變數都存盤在主記憶體(Main Memory)中,每條執行緒還有自己的作業記憶體(Working Memory,可與前面講的處理器高速快取類比),執行緒的作業記憶體中保存了被該執行緒使用的變數的主記憶體副本[2],執行緒對變數的所有操作(讀取、賦值等)都必須在作業記憶體中進行,而不能直接讀寫主記憶體中的資料[3],不同的執行緒之間也無法直接訪問對方作業記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成,
執行緒、主記憶體、作業記憶體三者的互動關系如下圖所示:

這里所講的主記憶體、作業記憶體與第2章所講的Java記憶體區域中的Java堆、堆疊、方法區等并不是同一個層次的對記憶體的劃分,這兩者基本上是沒有任何關系的,如果兩者一定要勉強對應起來,那么從變數、主記憶體、作業記憶體的定義來看,主記憶體主要對應于Java堆中的物件實體資料部分,而作業記憶體則對應于虛擬機堆疊中的部磁區域,從更基礎的層次上說,主記憶體直接對應于物理硬體的記憶體,而為了獲取更好的運行速度,虛擬機(或者是硬體、作業系統本身的優化措施)可能會讓作業記憶體優先存盤于暫存器和高速快取中,因為程式運行時主要訪問的是作業記憶體,
1.2 記憶體間互動操作
關于主記憶體與作業記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到作業記憶體、如何從作業記憶體同步回主記憶體這一類的實作細節,Java記憶體模型中定義了8 個操作來完成主記憶體和作業記憶體的互動操作:
- ① 首先是從
lock加鎖開始,作用于主記憶體的變數,把一個變數標識為一條執行緒獨占的狀態; - ②
read讀取,作用于主記憶體變數,將一個變數的值從主記憶體讀取到作業記憶體中; - ③
load加載,作用于作業記憶體的變數,把read讀取到的值加載到作業記憶體的變數副本中; - ④
use使用,作用于作業記憶體的變數,把作業記憶體中變數的值傳遞給執行引擎使用,每當虛擬機遇到一個需要使用變數值的位元組碼指令時將會執行這個操作; - ⑤
assign賦值,作用于作業記憶體的變數,把從執行引擎接收到的值賦值給作業記憶體的變數,每當虛擬機遇到一個需要使用變數值的位元組碼指令時將會執行這個操作; - ⑥
store存盤,作用于作業記憶體的變數,把作業記憶體中變數的值傳送回主記憶體中,以便隨后的write的操作; - ⑦
write寫入,作用于主記憶體的變數,把store得到的值放入主記憶體的變數中; - ⑧ 最后是
unlock解鎖,把主記憶體中處于鎖定狀態的變數釋放出來,流程到這一步就結束了,
如圖所示:

JMM 基本可以說是圍繞著在并發中如何處理這三個特性而建立起來的,也就是原子性、可見性、以及有序性,
如果要把一個變數從主記憶體拷貝到作業記憶體,那就要按順序執行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操作之前,必須先執行assign和load操作,
- 一個變數在同一個時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變數才會被解鎖,
- 如果對一個變數執行lock操作,那將會清空作業記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作以初始化變數的值,
- 如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數,
- 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store、write操作),
這8種記憶體訪問操作以及上述規則限定,再加上后面介紹的專門針對volatile的一些特殊規定,就已經能準確地描述出Java程式中哪些記憶體訪問操作在并發下才是安全的,
1.3 對于volatile型變數的特殊規則
關鍵字 volatile 可以說是Java虛擬機提供的最輕量級的同步機制,當一個變數被定義成 volatile 之后,它將具備兩項特性:
- 第一項是保證變數對所有執行緒的可見性,這里的“可見性”是指當一條執行緒修改了這個變數的值,新值對于其他執行緒來說是可以立即得知的,而普通變數并不能做到這一點,普通變數的值在執行緒間傳遞時均需要通過主記憶體來完成,比如,執行緒A修改一個普通變數的值,然后向主記憶體進行回寫,另外一條執行緒B在執行緒A回寫完成了之后再對主記憶體進行讀取操作,新變數值才會對執行緒B可見,
- 使用volatile變數的第二個語意是禁止指令重排序優化,普通的變數僅會保證在該方法的執行程序中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變數賦值操作的順序與程式代碼中的執行順序一致,
1.3.1 volatile保證可見性的使用場景
退不出的回圈:
先來看一個現象,main 執行緒對 run 變數的修改對于 t 執行緒不可見,導致了 t 執行緒無法停止:
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
首先 t 執行緒運行,然后過一秒,主執行緒設定 run 的值為 false,想讓 t 執行緒停止下來,但是 t 執行緒并沒有停!
為什么呢?來圖解分析一下:
-
初始狀態, t 執行緒剛開始從主記憶體讀取了 run 的值到作業記憶體,

-
.因為 t 執行緒要頻繁從主記憶體中讀取 run 的值,JIT 編譯器會將 run 的值快取至自己作業記憶體中的高 速快取中,減少對主存中 run 的訪問,提高效率

-
1 秒之后,main 執行緒修改了 run 的值,并同步至主存,而 t 是從自己作業記憶體中的高速快取中讀 取這個變數的值,結果永遠是舊值

解決方法:
volatile(關鍵字):
它可以用來修飾成員變數和靜態成員變數,他可以避免執行緒從自己的作業快取中查找變數的值,必須到 主存中獲取它的值,執行緒操作 volatile 變數都是直接操作主存,
public static volatile boolean run = true; // 保證記憶體的可見性
volatile保證可見性
前面例子體現的實際就是可見性,它保證的是在多個執行緒之間,一個執行緒對 volatile 變數的修改對另一 個執行緒可見, 不能保證原子性,僅用在一個寫執行緒,多個讀執行緒的情況: 上例從位元組碼理解是這樣的:
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
putstatic run // 執行緒 main 修改 run 為 false, 僅此一次
getstatic run // 執行緒 t 獲取 run false
比較一下之前我們將執行緒安全時舉的例子:兩個執行緒一個i++ 一個 i-- ,只能保證看到最新值,不能解 決指令交錯
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改后的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改后的值存入靜態變數i 靜態變數i
注意:
synchronized陳述句塊既可以保證代碼塊的原子性,也同時保證代碼塊內變數的可見性,但缺點是synchronized是屬于重量級操作,性能相對更低如果在前面示例的死回圈中加入
System.out.println()會發現即使不加volatile修飾符,執行緒 t 也 能正確看到對 run 變數的修改了,想一想為什么?(因為println()中有synchronized關鍵字加鎖,可以保證原子性與可見性,它是 PrintStream 類的方法 )
1.3.2 volatile保證有序性的使用場景
詭異的結果(指令重排):
首先看一個例子:
// 可以重排的例子
int a = 10;
int b = 20;
System.out.println( a + b );
// 不能重排的例子
int a = 10;
int b = a - 5;
指令重排簡單來說可以,在程式結果不受影響的前提下,可以調整指令陳述句執行順序,多執行緒下指令重排會影響正確性,
多執行緒下指令重排問題:
再分析下面的代碼:
int num = 0;
// volatile 修飾的變數,可以禁用指令重排 volatile boolean ready = false; 可以防止變數之前的代碼被重排序
boolean ready = false;
// 執行緒1 執行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
}
else {
r.r1 = 1;
}
}
// 執行緒2 執行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
I_Result 是一個物件,有一個屬性 r1 用來保存結果,問,可能的結果有幾種?
在多執行緒環境下,以上的代碼 r1 的值有三種情況:
-
情況1:執行緒1 先執行,這時
ready = false,所以進入 else 分支結果為 1 -
情況2:執行緒2 先執行
num = 2,但沒來得及執行ready = true,執行緒1 執行,還是進入 else 分支,結 果為1 -
情況3:執行緒 2 先執行,但是發送了指令重排,
num = 2與ready = true這兩行代碼語序發生裝換,ready = true; // 前 num = 2; // 后然后執行 ready = true 后,執行緒 1 運行了,那么 r1 的結果是為 0,
-
情況4:結果還有可能是 0
- 這種現象叫做指令重排,是 JIT 編譯器在運行時的一些優化,這個現象需要通過大量測驗才能偶爾遇見!
解決方法:
volatile 修飾的變數,可以禁用指令重排,禁止的是加volatile 關鍵字變數之前的代碼重排序,
1.3.3 volatile關鍵字是如何保證有序性的?
-
當一個共享變數被
volatile修飾時,它會保證修改的值會被立即更新到主記憶體中,當有其他執行緒讀取該值時,也不會直接讀取作業記憶體中的值,而是直接去主記憶體中讀取, -
而普通的共享變數不能保證可見性的,因為普通共享變數被修改后,寫入了作業記憶體中,什么時候寫入主記憶體其實是不可知的,當其他執行緒去讀取是,此時無論是作業記憶體還是主記憶體,可能還是原來的值,因此無法保證可見性,
被volatile關鍵字修飾的變數,在每個寫操作之后,都會加入一條store記憶體屏障命令,此命令強制將此變數的最新值從作業記憶體同步至主記憶體;在每個讀操作之前,都會加入一條load記憶體屏障命令,此命強制從主記憶體中將此變數的最新值加載至當前執行緒的作業記憶體中,
1.3.4 volatile關鍵字是如何保證有序性的?
volatile 可以禁止指令重排,保證程式會嚴格按照代碼的先后順序執行,
加了volatile 修飾的共享變數,通過記憶體屏障解決多執行緒下的有序性問題,原理如下:
- 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障
- 在每個 volatile 寫操作的后面插入一個StoreLoad屏障
- 在每個 volatile 讀操作的后面插入一個LoadLoad屏障
- 在每個 volatile 讀操作的后面插入一個LoadStore屏障
volatile 在寫操作前后插入了記憶體屏障后生成的指令序列示意圖如下:

volatile 在讀操作后面插入了記憶體屏障后生成的指令序列示意圖如下:

1.4 針對long和double型變數的特殊規則
Java記憶體模型要求lock、unlock、read、load、assign、use、store、write這八種操作都具有原子性,但是對于64位的資料型別(long和double),在模型中特別定義了一條寬松的規定:允許虛擬機將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實作自行選擇是否要保證64位資料型別的load、store、read和write這四個操作的原子性,這就是所謂的“long和double的非原子性協定”(Non-Atomic Treatment of double and long Variables),
如果有多個執行緒共享一個并未宣告為volatile的long或double型別的變數,并且同時對它們進行讀取和修改操作,那么某些執行緒可能會讀取到一個既不是原值,也不是其他執行緒修改值的代表了“半個變數”的數值,不過這種讀取到“半個變數”的情況是非常罕見的,經過實際測驗[1],在目前主流平臺下商用的64位Java虛擬機中并不會出現非原子性訪問行為,但是對于32位的Java虛擬機,譬如比較常用的32位x86平臺下的HotSpot虛擬機,對long型別的資料確實存在非原子性訪問的風險,
1.5 先行發生原則
“先行發生”(Happens-Before)原則,它是判斷資料是否存在競爭,執行緒是 否安全的非常有用的手段,
先行發生是Java記憶體模型中定義的兩項操作之間的偏序關系,比如說操作A先行發生于操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共享變數的值、發送了訊息、呼叫了方法等,
先行發生原則示例:
// 以下操作在執行緒A中執行
i = 1;
// 以下操作在執行緒B中執行
j = i;
// 以下操作在執行緒C中執行
i = 2;
假設執行緒A中的操作“i=1”先行發生于執行緒B的操作“j=i”,那我們就可以確定在執行緒B的操作執行后,變數j的值一定是等于1,得出這個結論的依據有兩個:一是根據先行發生原則,“i=1”的結果可以被觀察到;二是執行緒C還沒登場,執行緒A操作結束之后沒有其他執行緒會修改變數i的值,現在再來考慮執行緒C,我們依然保持執行緒A和B之間的先行發生關系,而C出現在執行緒A和B的操作之間,但是C與B沒有先行發生關系,那j的值會是多少呢?答案是不確定!1和2都有可能,因為執行緒C對變數i的影響可能會被執行緒B觀察到,也可能不會,這時候執行緒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的結論,
2. Java與執行緒
2.1 執行緒的實作
實作執行緒主要有三種方式:使用內核執行緒實作(1:1實作),使用用戶執行緒實作(1:N實作),使用用戶執行緒加輕量級行程混合實作(N:M實作),
這三種方式詳細介紹小伙伴可以自行查閱資料,本文這塊知識介紹不作為重點,
2.2 Java執行緒調度
執行緒調度是指系統為執行緒分配處理器使用權的程序,調度主要方式有兩種,分別是協同式(Cooperative Threads-Scheduling)執行緒調度和搶占式(Preemptive Threads-Scheduling)執行緒調度,
2.3 執行緒狀態轉換
Java語言定義了6種執行緒狀態,在任意一個時間點中,一個執行緒只能有且只有其中的一種狀態,并且可以通過特定的方法在不同狀態之間轉換,這6種狀態分別是:
- 新建(New):創建后尚未啟動的執行緒處于這種狀態,
- 運行(Runnable):包括作業系統執行緒狀態中的Running和Ready,也就是處于此狀態的執行緒有可能正在執行,也有可能正在等待著作業系統為它分配執行時間,
- 無限期等待(Waiting):處于這種狀態的執行緒不會被分配處理器執行時間,它們要等待被其他執行緒顯式喚醒,以下方法會讓執行緒陷入無限期的等待狀態:
- 沒有設定Timeout引數的Object::wait()方法;
- 沒有設定Timeout引數的Thread::join()方法;
- LockSupport::park()方法,
- 限期等待(Timed Waiting):處于這種狀態的執行緒也不會被分配處理器執行時間,不過無須等待被其他執行緒顯式喚醒,在一定時間之后它們會由系統自動喚醒,以下方法會讓執行緒進入限期等待狀態:
- Thread::sleep()方法;
- 設定了Timeout引數的Object::wait()方法;
- 設定了Timeout引數的Thread::join()方法;
- LockSupport::parkNanos()方法;
- LockSupport::parkUntil()方法,
- 阻塞(Blocked):執行緒被阻塞了,“阻塞狀態”與“等待狀態”的區別是“阻塞狀態”在等待著獲取到一個排它鎖,這個事件將在另外一個執行緒放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生,在程式等待進入同步區域的時候,執行緒將進入這種狀態,
- 結束(Terminated):已終止執行緒的執行緒狀態,執行緒已經結束執行,
上述6種狀態在遇到特定事件發生的時候將會互相轉換,它們的轉換關系如下圖所示:

面試題參考
- JMM 記憶體模型、volatile 關鍵字保證有序性和可見性相關問題總結
結語:
非常建議學習Java的小伙伴,買一本周志明老師的《深入理解Java虛擬機(第3版)》去讀一讀,博客和視頻教程,始終不如看書來得實在呀!
后續會陸續更新,這本書的筆記記的差不多了,排版和格式需要花時間整理,文章都會同步到公眾號上,也歡迎大家通過公眾號加入我的交流qun互相討論jvm這塊的知識內容!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/296925.html
標籤:其他
