目錄
一、背景
二、Java線程模型
三、Synchronized實現原理
3.1 鎖的使用
3.2 解釋執行
3.3 JIT執行
3.4 鎖的狀態
3.5?monitorenter
3.5.1 偏向鎖
3.5.2 輕量級鎖
3.5.3 重量級鎖
3.6?monitorexit
3.6.1 偏向鎖
3.6.2 輕量級鎖
3.6.3 重量級
四、可見性的真相
4.1 緩存一致性協議
4.2 根因分析
五、CAS實現原理
5.1 CAS介紹
5.2 CAS特性
5.3 CAS實現原理
六、Volatile
6.1 字節碼
6.2 屏障類型
6.3?volatile修飾的寫操作
6.4?volatile修飾的讀操作
6.5 如何保證可見性
七、Reentrantlock實現原理
7.1 AQS
7.2 lock
7.3 unlock
7.4 CLH隊列
7.5 與?synchronized?關鍵字的對比
7.5 可中斷鎖和超時鎖
7.6 Condition
八、線程阻塞或喚醒時操作系統的動作
九、原子性、有序性、可見性的保證
一、背景
講解一下在java中涉及到并發的相關基礎知識,深入理解 synchronized, volatile, CAS, ReentrantLock 與內存可見性、原子性、有序性
二、Java線程模型
現代JVM(如HotSpot)默認采用此模型:每個Java線程直接綁定一個操作系統線程(內核線程)
-
優點:
-
利用多核CPU并行執行。
-
線程阻塞(如I/O)不影響其他線程。
-
-
缺點:
-
線程創建/銷毀開銷大(需OS介入)。
-
線程數量受限于OS(默認Linux約數千個)。
-
三、Synchronized實現原理
3.1 鎖的使用
Java 使用synchronized關鍵字加鎖,
Object lock = new Object();
synchronized (lock) {// 代碼塊
}
加上鎖的代碼會被編譯成如下的字節碼:
0: new #2 // 創建 Object 對象
3: dup
4: invokespecial #1 // 調用 Object 構造方法
7: astore_1 // 存儲到局部變量 lock8: aload_1 // 加載 lock 到操作數棧
9: dup
10: astore_2 // 存儲鎖對象副本
11: monitorenter // 嘗試獲取鎖
12: aload_2
13: monitorexit // 正常釋放鎖
14: goto 20
17: aload_2
18: monitorexit // 異常時釋放鎖 (確保鎖被釋放)
19: athrow
20: return
我們后面主要研究下?monitorenter? 和??monitorexit 的底層原理,了解這兩個字節碼,我們首先要了解,它們是如何被java執行的
3.2 解釋執行
“解釋執行”的核心含義:
-
執行單元是單條字節碼指令:?解釋器(包括模板解釋器)的基本工作單元是一條字節碼指令。它逐條讀取、解釋(分派)并執行字節碼指令。
-
沒有預先的“完整編譯”:?在執行一個 Java 方法之前,解釋器不會把這個方法包含的所有字節碼指令作為一個整體編譯成一段完整的、連續的、優化過的本地機器碼。它只為每條指令準備了模板。
-
邊“解釋”(分派)邊執行:?執行過程始終伴隨著一個分派循環:
-
取指:?從當前方法的字節碼流中讀取下一條指令的操作碼。
-
分派:?根據這個操作碼,查找或計算對應的機器碼模板的入口地址。
-
跳轉執行:?跳轉到該模板地址,執行對應的機器碼片段。
-
循環:?執行完這條指令的模板后,必須回到分派循環的開頭,重復上述步驟處理下一條指令。
-
-
執行上下文依賴分派循環:?每條指令的機器碼模板執行完畢后,控制權必須交還給解釋器的分派循環。這個循環負責維護執行狀態(如程序計數器 PC、棧指針等),并決定下一條要執行的指令是什么。模板本身通常不包含跳轉到下一條指令的邏輯(除了像?
goto
?這種控制流指令,它們會直接修改 PC)。
3.3 JIT執行
-
JIT 編譯:?當 HotSpot 發現某個方法是“熱點”時,它的 JIT 編譯器(如 C1, C2)會介入。
-
執行單元:?它將整個方法(或一個熱點循環)作為一個單元。
-
過程:?讀取該方法的所有相關字節碼,進行復雜的靜態分析和優化(如寄存器分配、死代碼消除、循環展開、方法內聯、逃逸分析等),最終生成一段完整的、連續的、高度優化的本地機器碼。
-
執行:?當再次調用這個方法時,JVM?直接跳轉到這段編譯好的機器碼的起始地址。這段機器碼自己負責執行整個方法的邏輯,包括控制流(跳轉、循環、調用)。它完全繞過了解釋器的分派循環。執行過程中不再有“取指-分派”的開銷,并且代碼是優化過的。
-
-
關鍵區別:?JIT 編譯后執行的是一個完整優化后的代碼塊,而模板解釋器執行的是一系列獨立的、通過分派循環粘合起來的機器碼片段。
通過上面的解釋,我們大概能理解,java執行時是邊解釋字節碼邊執行的,而不是直接翻譯成機器碼文件。
java會把?monitorenter? 和??monitorexit解釋成相關的機器碼,JVM執行時會跳到monitorenter 的機器碼的位置,進行執行
例如對于monitorenter? :
-
當解釋器執行到?
monitorenter
?字節碼時:-
分派循環跳轉到?
monitorenter
?的機器碼模板入口。 -
CPU 執行模板中的指令:準備好參數(對象引用),然后執行?
call
?指令。 -
CPU 跳轉到?
InterpreterRuntime::monitorenter
?函數的機器碼(這個函數本身是 C++ 寫的,在 JVM 啟動時已經被編譯成了機器碼)。 -
InterpreterRuntime::monitorenter
?的機器碼執行復雜的鎖邏輯(可能調用更底層的?ObjectMonitor::enter
?等)。 -
InterpreterRuntime::monitorenter
?函數執行完畢,通過?ret
?指令返回到調用它的地方——也就是?monitorenter
?模板中?call
?指令的下一條指令。 -
monitorenter
?模板中剩余的指令(如果有)執行。 -
控制權返回到解釋器的分派循環,準備執行下一條字節碼。
-
下面來看下monitorenter和monitorexit的具體動作
3.4 鎖的狀態
📐 對象頭組成及大小(以主流64位系統為例)
??組成部分?? | ??大小(字節)?? | ??說明?? | ??是否可變?? |
---|---|---|---|
??Mark Word?? | 8 | 存儲對象哈希碼、鎖狀態、GC年齡等信息 | ? 固定大小 |
??Klass Pointer?? | 4(或8) | 指向類元數據的指針(默認指針壓縮) | ? 可配置 |
??數組長度(可選)?? | 4 | 僅數組對象存在(存儲數組長度) | ? 固定大小 |
在不同鎖狀態下,保存內容如下所示
3.5?monitorenter
java8的鎖粒度有 偏向鎖、輕量級鎖、重量級鎖
3.5.1 偏向鎖
-
檢查對象頭中的?Mark Word:
-
若可偏向(偏向模式位?
1
,鎖標志位?01
)且?線程ID指向當前線程:直接進入同步塊(無CAS)。 -
若可偏向但?線程ID不指向當前線程:觸發偏向鎖撤銷(需全局安全點),升級為輕量級鎖。
-
若未偏向:通過?CAS?將 Mark Word 的線程ID設置為當前線程。
-
撤銷偏向鎖過程:
-
觸發條件
當線程A嘗試獲取偏向線程B的鎖時(對象頭Mark Word中的線程ID ≠ 當前線程ID),觸發撤銷。 -
暫停所有Java線程(STW)
-
JVM 觸發?全局安全點(Safepoint),暫停所有Java線程(包括持有偏向鎖的線程B和競爭線程A)。
-
關鍵原因:對象頭的Mark Word和持有鎖線程B的棧幀狀態需被原子修改,避免并發沖突。
-
-
撤銷操作(由JVM在安全點執行)
-
步驟1:檢查持有偏向鎖的線程B的狀態:
-
若線程B?已退出同步塊(無活躍鎖):
-
直接重置對象頭為?無鎖狀態(鎖標志位?
01
,偏向模式?0
)。
-
-
若線程B?仍處于同步塊中(活躍鎖):
-
將鎖升級為?輕量級鎖:
-
在線程B的棧幀中生成鎖記錄(Lock Record),拷貝原Mark Word。
-
用CAS將對象頭的Mark Word替換為指向該鎖記錄的指針(鎖標志位?
00
)。
-
-
若線程B已銷毀:強制釋放偏向鎖。
-
-
-
-
恢復線程并升級競爭
-
安全點結束后,所有線程恢復執行。
-
競爭線程A重新嘗試獲取鎖:
-
此時對象頭已是輕量級鎖狀態(標志位?
00
),線程A通過?CAS自旋?競爭輕量級鎖。
-
-
3.5.2 輕量級鎖
-
在棧幀中創建?鎖記錄(Lock Record),拷貝對象頭的 Mark Word(稱為?Displaced Mark Word)。
-
通過?CAS?將對象頭的 Mark Word 替換為指向鎖記錄的指針(鎖標志位?
00
)。-
成功:獲得鎖。
-
失敗(其他線程已占用):自旋重試;若自旋失敗,升級為重量級鎖。
-
cas操作的值如下:
-
目標內存地址:對象頭的 Mark Word
-
預期值(Expected Value):對象原始的 Mark Word(從棧幀的鎖記錄中獲取),對象原始的 Mark Word一般是偏向鎖或者無鎖狀態
-
新值(New Value):指向當前線程棧幀中鎖記錄的指針 + 輕量級鎖標志位?
00
3.5.3 重量級鎖
-
觸發條件:
當線程嘗試獲取輕量級鎖時,如果 CAS 失敗(表示鎖已被占用),線程會進入自旋重試狀態。-
若自旋超過閾值仍失敗(默認約 10-50 次,JVM 自適應調整)
-
或自旋期間有第三個線程加入競爭
-
參考圖:
-
升級動作:
JVM 調用?inflateLock()
?方法,創建?ObjectMonitor
?對象(重量級鎖結構),修改對象頭標志位為?10
Monitor 對象對象結構:
-
檢查對象關聯的?Monitor 對象(位于對象頭指向的?ObjectMonitor)。
-
調用?
ObjectMonitor::enter()
:-
若 Owner 為?
null
:通過 CAS 設置 Owner 為當前線程。 -
若 Owner 是當前線程:重入計數?
_recursions++
。 -
否則:線程進入?阻塞隊列(cxq/EntryList),等待喚醒。
-
3.6?monitorexit
3.6.1 偏向鎖
-
不修改對象頭(保留偏向狀態)。
-
僅檢查線程ID是否匹配(防止錯誤解鎖)。
3.6.2 輕量級鎖
通過?CAS?將?Displaced Mark Word?寫回對象頭。
-
成功:鎖釋放。
-
失敗:表明已升級為重量級鎖,需走重量級鎖釋放流程
3.6.3 重量級
-
調用?
ObjectMonitor::exit()
:-
重入計數?
_recursions--
。 -
若?
_recursions == 0
:清空 Owner,喚醒阻塞隊列中的線程。
-
重量級鎖變化舉例:
四、可見性的真相
4.1 緩存一致性協議
多線程并發修改變量時會有可見性問題
雖然我們有緩存一致性協議,具體如下圖,但它只能保證最終一致性,而不能保證中間過程的一致性:
4.2 根因分析
已知:
當CPU修改共享變量時:
-
使其他CPU的緩存行失效(I狀態)
-
修改自己的緩存(M狀態)
-
最終寫回主內存
分析:
原因1:寫緩沖(Store Buffer)
現代CPU使用寫緩沖優化性能:
plaintext
CPU-A 操作流程: 1. b的新值存入寫緩沖 ← 立即繼續執行后續指令 2. 異步將寫緩沖刷新到緩存(此時才觸發MESI)
在步驟1→2的間隙中:
-
CPU-C讀取b時,由于失效請求未到達,可能仍讀取自己的舊緩存
-
即使CPU-A認為"修改已完成",實際修改仍在緩沖中未提交
原因2:失效隊列(Invalidation Queue)
plaintext
CPU-C 操作流程: 1. 收到b的失效請求 → 存入失效隊列 2. 繼續使用本地緩存(直到處理失效隊列) ← 關鍵延遲點! 3. 后續讀取才從主內存重新加載
在步驟1→3期間,CPU-C仍可能使用已失效的緩存值。
原因3:指令重排:
// 共享變量
int a = 0;
boolean flag = false; // 注意:非volatile!// 線程1(核心1執行)
a = 42; // 語句1
flag = true; // 語句2// 線程2(核心2執行)
while(!flag); // 語句3
System.out.println(a); // 可能輸出0!
具體如何解決可見性問題,我們后面分析
五、CAS實現原理
5.1 CAS介紹
CAS(Compare-And-Swap)?是一種基于硬件的原子操作,用于實現無鎖(lock-free)并發編程。它是 Java 并發包(java.util.concurrent
)中原子類(如?AtomicInteger
、AtomicReference
?等)的核心實現機制。
CAS 操作包含三個參數:
-
內存地址?
V
(需要更新的變量) -
期望值?
A
(變量當前應具有的值) -
新值?
B
(需要設置的新值)
操作邏輯:
java
if (V == A) {V = B; // 更新值return true; // 成功 } else {return false; // 失敗 }
整個過程由硬件(CPU 指令)保證原子性,不會被線程調度打斷。
5.2 CAS特性
-
原子性(Atomicity)
-
操作不可分割,要么完全執行成功,要么完全不執行。
-
由底層 CPU 指令(如 x86 的?
CMPXCHG
)直接支持,無需鎖。
-
-
無鎖(Lock-Free)
-
線程通過循環重試(自旋)更新數據,避免阻塞。
-
示例代碼:
java
public final int incrementAndGet(AtomicInteger atomicInt) {int prev, next;do {prev = atomicInt.get(); // 當前值next = prev + 1; // 新值} while (!atomicInt.compareAndSet(prev, next)); // CAS 失敗則重試return next; }
-
-
可見性(Visibility)
-
CAS 操作隱含?
volatile
?語義,確保修改對其他線程立即可見。
-
-
避免死鎖
-
無鎖機制天然規避了死鎖風險。
-
5.3 CAS實現原理
由底層 CPU 硬件指令直接保證的
-
x86/x86-64 架構
-
指令:
LOCK CMPXCHG
-
CMPXCHG
(Compare and Exchange)是基礎指令 -
LOCK
?前綴(匯編指令前綴)強制獨占內存訪問,確保原子性 -
工作流程:
assembly
; 偽匯編代碼 LOCK CMPXCHG [memory], reg ; [memory]為內存地址,reg為新值 ; 比較 EAX(隱含寄存器)與 [memory] 的值 ; 相等 → 將 reg 存入 [memory],并設置 ZF=1 ; 不等 → 將 [memory] 加載到 EAX,并設置 ZF=0
-
-
-
總線鎖定(Bus Locking)
-
LOCK
?前綴使 CPU 在執行期間鎖定內存總線 -
阻止其他核心/CPU 訪問同一內存區域
-
注意:CPU 在成功將緩存行標記為“獨占”(Exclusive)狀態之前,必須確保它擁有該緩存行的最新數據副本。如果它發現數據不是最新的,它就無法成功獲得獨占狀態,操作會失敗或需要重試。
-
緩存一致性協議(如 MESI)
-
現代 CPU 通過緩存鎖定代替總線鎖定
-
當 CPU 檢測到?
CMPXCHG
?操作時:-
將緩存行標記為"獨占"狀態
-
若其他核心嘗試修改,會使其緩存行失效
-
確保只有一個核心能成功修改
-
-
-
內存順序模型支持
-
指令隱含內存屏障(Memory Barrier)
-
保證操作前后的內存可見性順序
-
六、Volatile
6.1 字節碼
代碼示例:
public class Test {volatile int v;
}
字節碼:Field access_flags: 0x0040 (ACC_VOLATILE)
對于?v = 42
(volatile 寫),字節碼只有一條簡單指令:
java
putfield #4 // 將值 42 寫入字段 v(#4 是常量池索引)
關鍵點:
-
字節碼?沒有顯式的屏障指令
-
JVM 通過字段的?
ACC_VOLATILE
?標志識別需要特殊處理
6.2 屏障類型
JVM 通過內存屏障實現?volatile
?的語義,屏障類型如下:
屏障類型 | 作用 |
---|---|
LoadLoad | 確保當前讀操作在后續讀操作之前完成。 |
StoreStore | 確保當前寫操作在后續寫操作之前完成(刷新到主內存)。 |
LoadStore | 確保當前讀操作在后續寫操作之前完成。 |
StoreLoad | 全能屏障:確保當前寫操作對所有處理器可見后才執行后續讀/寫操作。 |
volatile
?讀寫操作的屏障插入規則:
-
volatile
?寫操作:-
寫操作前:
StoreStore
?屏障(防止與前面的普通寫重排序)。 -
寫操作后:
StoreLoad
?屏障(防止與后面的?volatile
?讀/寫重排序)。
java
StoreStore Barrier v = 42; // volatile 寫 StoreLoad Barrier
-
-
volatile
?讀操作:-
讀操作前:
LoadLoad
?+?LoadStore
?屏障(防止與后續操作重排序)。 -
讀操作后:無額外屏障(某些架構下合并到前面)。
java
int tmp = v; // volatile 讀 LoadLoad Barrier + LoadStore Barrier
-
具體對應的CPU指令不寫了,過程有點復雜,大概率了解一下吧
屏障類型較多,理解起來也有點費勁,我直接說結果,即有了這些屏障后,程序的運行結果是怎樣的
6.3?volatile修飾的寫操作
// 第一階段:準備操作(普通讀寫) 普通操作1 普通操作2 ... 普通操作N// StoreStore 屏障(隱形防線) volatile 寫 = 新值; // 關鍵操作// StoreLoad 屏障(隱形防線) 后續操作1 后續操作2 ... 后續操作M
1.?屏障前的保證(前置防護)
-
??所有 volatile 之前的操作都已計算完成
(包括普通讀/寫、方法調用等) -
??所有普通寫操作結果全局可見
(通過 StoreStore 屏障保證) -
??絕不允許 volatile 之后的操作重排到前面
(StoreLoad 屏障阻止后續任何操作前移)
2.?屏障后的保證(后置防護)
-
??volatile 寫本身全局可見
(通過 StoreLoad 屏障強制刷新) -
??絕不允許 volatile 之前的操作重排到后面
(StoreStore 屏障阻止前面操作后移)
6.4?volatile修飾的讀操作
// 前置操作區(可能被重排到此)
普通操作A
普通操作B
// 墻入口
int tmp = v;? // volatile讀
// 隱形的雙屏障防線
LoadLoad Barrier
LoadStore Barrier
// 后置保護區
后續操作1
后續操作2
1.?墻的入口(讀操作本身)
-
??強制獲取最新值:
使當前 CPU 緩存失效,從主內存加載最新值 -
???不限制前面操作:
允許墻前的普通操作重排到墻后(與寫操作關鍵區別!)
6.5 作用總結
一、volatile
?寫操作的作用
當線程執行?volatile
?寫操作(如?volatileVar = 42;
)時:
-
可見性保證
-
? 確保該寫操作完成后,所有線程都能立即看到這個新值。
-
🔧 底層機制:強制刷新 CPU 寫緩沖區,將新值同步到主內存,并通過緩存一致性協議(如 MESI)使其他 CPU 的緩存副本失效。
-
-
有序性保證
-
? 禁止指令重排序:
-
禁止將?
volatile
?寫之前的任何操作?重排序到寫操作之后。 -
禁止將?
volatile
?寫之后的讀操作?重排序到寫操作之前。
-
-
🔧 底層機制:插入?
StoreStore
?+?StoreLoad
?內存屏障(如 x86 的?lock
?指令)。
-
二、volatile
?讀操作的作用
當線程執行?volatile
?讀操作(如?int val = volatileVar;
)時:
-
可見性保證
-
? 確保讀取到的是最新值(可能是其他線程剛寫入的值)。
-
🔧 底層機制:強制從主內存或其他 CPU 重新加載數據(跳過本地可能過期的緩存)。
-
-
有序性保證
-
? 禁止指令重排序:
-
禁止將?
volatile
?讀之后的任何操作?重排序到讀操作之前。 -
禁止將?
volatile
?讀之前的寫操作?重排序到讀操作之后。
-
-
🔧 底層機制:插入?
LoadLoad
?+?LoadStore
?內存屏障(如 ARM 的?dmb
?指令)。
-
6.6 x86 CPU實現
以下是幾種常見架構上,為了實現?volatile
?語義,HotSpot JVM 通常使用的屏障和對應的 CPU 指令:
-
-
x86/x86-64:
-
volatile
?寫:-
屏障要求:?StoreStore 屏障 + StoreLoad 屏障
-
實際指令:?
lock addl $0x0, (%rsp)
?(或其他類似指令)-
lock
?前綴:這是 x86 上實現強內存屏障效果的關鍵。它鎖定內存總線(或使用緩存一致性協議如 MESI),確保該指令的操作具有原子性,并隱式包含了 StoreLoad 屏障。它還會刷新寫緩沖區。 -
addl $0x0, (%rsp)
:這是一個對棧頂指針?%rsp
?指向的內存地址加 0 的空操作。它本身不改變數據,目的是提供一個讓?lock
?前綴作用的目標指令。 -
為什么不需要顯式 StoreStore??x86 架構的 TSO (Total Store Order) 內存模型本身保證了寫操作(包括非 volatile 寫)不會重排序。因此,在?
volatile
?寫之前插入的 StoreStore 屏障在 x86 上通常是?空操作(nop
)。
-
-
-
volatile
?讀:-
屏障要求:?LoadLoad 屏障 + LoadStore 屏障
-
實際指令:?通常沒有顯式的屏障指令!
-
原因:?x86 的 TSO 模型天然保證了:
-
LoadLoad 不會重排序(后面的讀能看到前面的讀的結果)。
-
LoadStore 不會重排序(后面的寫不會重排序到前面的讀之前)。
-
-
因此,對于?
volatile
?讀,JVM 在 x86 上通常只需要生成普通的?mov
?指令來加載值,而不需要插入任何顯式的屏障指令。volatile
?讀本身的內存語義(如緩存行失效)由 CPU 的緩存一致性協議(MESI)自動處理。
-
-
-
總結 (x86):
-
volatile
?寫:lock addl $0x0, (%rsp)
?(或等效指令) -> 主要提供?StoreLoad 屏障。 -
volatile
?讀:普通的?mov
?指令 ->?屏障是空操作。
-
-
-
Q:那lock addl $0x0, (%rsp) 指令和 修改 volatile值的mov指令 不是原子的,豈不是會造成值修改了,但是不可見的情況?
lock addl $0x0, (%rsp)
?的核心作用是:
-
排空寫緩沖區
強制當前 CPU 的寫緩沖區中所有數據(包括之前的?mov
)立即刷到緩存/內存。 -
使其他 CPU 的緩存失效
通過緩存一致性協議(MESI)廣播,使其他 CPU 中該 volatile 變量的緩存行失效。 -
StoreLoad 屏障
確保后續讀操作必須重新從主存加載最新值。
假設?mov
?執行后、lock
?執行前發生中斷:
x86asm
mov [var], eax ; 值進入寫緩沖區 ; <-- 此處發生中斷(寫緩沖區未刷新) lock addl $0x0, (%rsp) ; 中斷返回后執行
-
問題:其他 CPU 在中斷期間可能讀到舊值。
-
解答:x86 的中斷處理機制保證:
-
中斷返回前會隱式排空寫緩沖區(類似?
sfence
)。 -
中斷結束后執行?
lock
?指令,再次強制刷新并失效化緩存。
-
-
結果:最終仍保證可見性(可能略有延遲,但符合 Java 語義)。
七、Reentrantlock實現原理
7.1 AQS
Reentrantlock基于AQS實現,AQS介紹
-
狀態變量 (
state
):-
一個?
volatile int
?類型的變量,表示鎖的狀態。 -
對于?
ReentrantLock
:-
state = 0
: 鎖未被任何線程持有。 -
state > 0
: 鎖已被某個線程持有。數值表示該線程重入鎖的次數(同一個線程多次獲取鎖)。
-
-
-
CLH 隊列 (FIFO 線程等待隊列):
-
一個雙向鏈表(或變體)實現的等待隊列。
-
當多個線程競爭鎖失敗時,它們會被構造成?
Node
?節點,并加入到這個隊列尾部排隊等待。 -
隊列中的線程會以 FIFO(先進先出)的順序被喚醒(公平模式下嚴格 FIFO,非公平模式下可能插隊)。
-
7.2 lock
-
-
嘗試獲取 (
tryAcquire
):-
線程首先嘗試通過 CAS 操作將?
state
?從 0 改為 1。 -
成功 (state 原來是 0):
-
設置當前線程為鎖的獨占所有者 (
exclusiveOwnerThread
)。 -
返回 true,獲取鎖成功。
-
-
失敗 (state > 0):
-
檢查當前線程是否已經是鎖的持有者 (
exclusiveOwnerThread == currentThread
)。 -
如果是,則將?
state
?加 1(重入計數增加),返回 true。 -
如果不是,返回 false。
-
-
-
加入隊列等待 (
acquireQueued
):-
如果?
tryAcquire
?失敗(返回 false),線程會將自己包裝成一個?Node
?節點。 -
使用 CAS 操作將節點安全地加入到 CLH 隊列的尾部。
-
進入自旋或阻塞狀態:
-
檢查自己是否是隊列中第一個有效等待節點(頭節點的后繼)。
-
如果是,再次嘗試?
tryAcquire
?(非公平鎖總是嘗試一次;公平鎖嚴格排隊)。 -
如果還不是第一個節點或嘗試失敗:
-
檢查前驅節點的狀態,判斷是否需要阻塞自己。
-
調用?
LockSupport.park(this)
?將當前線程掛起(阻塞)。
-
-
線程被喚醒(通常是前驅節點釋放鎖時喚醒它)后,會再次嘗試獲取鎖。
-
-
-
7.3 unlock
-
嘗試釋放 (
tryRelease
):-
檢查當前線程是否是鎖的持有者(
exclusiveOwnerThread == currentThread
),否則拋出?IllegalMonitorStateException
。 -
將?
state
?減 1(表示減少一次重入計數)。 -
如果?
state
?減到 0:-
將?
exclusiveOwnerThread
?設置為?null
,表示鎖完全釋放。 -
返回 true。
-
-
如果?
state
?仍然大于 0(說明還有重入),返回 false。
-
-
喚醒后繼 (
unparkSuccessor
):-
如果?
tryRelease
?返回 true(鎖完全釋放),則找到 CLH 隊列中第一個狀態正常的等待節點(通常是頭節點的后繼)。 -
調用?
LockSupport.unpark(s.thread)
?喚醒該節點對應的線程,使其有機會去競爭鎖。
-
相關代碼:
7.4 CLH隊列
前面加鎖和解鎖過程都使用了到了CLH隊列,下面具體介紹一下什么是CLH隊列,以及在Reentrantlock中做了哪些優化
-
核心思想:?一個隱式鏈接的 FIFO 隊列,用于管理等待鎖(或共享資源)的線程。每個等待線程被封裝成一個節點(Node)。
-
關鍵機制:?每個節點通過一個?
volatile
?狀態字段(通常是?waitStatus
)?來輪詢(spin)?其前驅節點(predecessor node)?的狀態。 -
核心操作:
-
入隊(獲取鎖失敗時):?新線程嘗試獲取鎖失敗后,會創建一個新節點,通過?
CAS
?(Compare-And-Swap)?操作將自身設置為新的?tail
(尾指針),同時記錄下它入隊時看到的尾節點作為其前驅節點(prev)。 -
等待(自旋輪詢前驅):?線程在一個循環中不斷檢查其前驅節點的狀態標志(
waitStatus
)。如果前驅節點的狀態表明它已經釋放了鎖(或即將釋放),那么當前線程就有資格嘗試獲取鎖。關鍵點:線程只關心它前面的那個節點(前驅)的狀態。 -
出隊(獲取鎖成功):?當線程檢測到前驅節點釋放了鎖(狀態變為?
SIGNAL
?或類似),它成功獲取鎖,并成為新的隊列頭(head
)。原頭節點通常被移除或成為虛擬頭節點。 -
釋放鎖(通知后繼):?當持有鎖的線程(隊列頭節點)釋放鎖時,它會檢查其后繼節點(
next
)的狀態。如果后繼節點在等待(狀態為?SIGNAL
),它會修改自身的狀態(例如,設置?waitStatus = 0
?或直接清除狀態)或直接設置后繼節點的狀態(在 AQS 變體中通常是設置頭節點狀態為?SIGNAL
?后由后繼輪詢),這個狀態變化會被其后繼節點(正在輪詢前驅狀態)立即感知(volatile
?保證可見性),從而喚醒后繼節點去嘗試獲取鎖。
-
-
原始 CLH 特點(純自旋):
-
FIFO 公平性。
-
線程在 CPU 上忙等(自旋)其前驅的狀態變化。
-
鎖釋放僅需修改一個?
volatile
?變量(自身狀態),通知是無鎖且高效的。 -
避免了“驚群效應”(只喚醒一個后繼)。
-
AQS中如何使用?
-
顯式雙向鏈表:
-
AQS 節點不僅維護指向前驅節點(
prev
)?的指針(用于輪詢狀態和取消時移除),還維護指向后繼節點(next
)?的指針。這不是原始 CLH 必需的(原始 CLH 通常只有隱式前驅鏈),但大大簡化了節點取消(Cancellation)(中斷、超時)時的鏈表操作。
-
-
虛擬頭節點(Dummy Head):
-
AQS 隊列初始化時通常會創建一個不關聯任何線程的虛擬頭節點。第一個真正等待的線程節點會成為虛擬頭節點的后繼。這使得隊列操作(如判斷是否有等待線程、喚醒后繼)邏輯更統一,避免邊界條件判斷。
-
-
狀態整合(
waitStatus
):-
AQS 節點的?
waitStatus
?字段承載了比原始 CLH 狀態標志更豐富的含義:-
SIGNAL(-1)
:?最重要的狀態。?表示該節點的后繼節點需要被喚醒(即,后繼節點在等待)。當前節點釋放鎖或取消時,必須喚醒其后繼節點。節點在阻塞自己之前,通常會將其前驅節點的?waitStatus
?設置為?SIGNAL
(通過?CAS
),這樣前驅節點就知道“我后面有人等著呢,你釋放時記得叫我”。 -
CANCELLED(1)
: 節點關聯的線程已取消等待(如超時或中斷)。需要從隊列中安全移除。 -
CONDITION(-2)
: 節點當前在條件隊列(ConditionObject
)中等待,而不是在主同步隊列中。 -
PROPAGATE(-3)
:?僅用于共享模式。?表示下一次?acquireShared
?操作應該無條件傳播(表示后續節點也可能可以獲取共享資源)。 -
0
: 初始狀態,或表示節點不處于上述任何特殊狀態。
-
-
-
阻塞代替自旋(關鍵優化):
-
這是 AQS CLH 變體最核心的改進!?原始 CLH 是純自旋(忙等),消耗 CPU。
-
AQS 中,線程不會持續自旋輪詢前驅狀態。其流程是:
-
a) 嘗試獲取鎖。
-
b) 失敗,創建節點并入隊(設置?
tail
,鏈接?prev
)。 -
c)?快速自旋檢查前驅狀態:?在一個循環中快速檢查幾次:
-
如果前驅是頭節點(說明快輪到自己了),再次嘗試獲取鎖(防止不必要的阻塞)。
-
如果前驅節點的?
waitStatus == SIGNAL
,說明前驅已設置好“釋放時喚醒我”的標志,安全地阻塞自己(調用?LockSupport.park()
)。 -
如果前驅狀態是?
CANCELLED
,則跳過該前驅,繼續找更前面的有效前驅。 -
如果前驅狀態正常但不是?
SIGNAL
,則嘗試用?CAS
?將前驅的?waitStatus
?設置為?SIGNAL
(告訴它“你釋放時記得叫我”)。
-
-
d) 如果步驟 c 中的檢查表明可以安全阻塞了(前驅是?
SIGNAL
),則調用?park()
?掛起當前線程。
-
-
喚醒:?當持有鎖的線程(頭節點)釋放鎖時,它會檢查頭節點的?
waitStatus
。如果是?SIGNAL
(通常都會是,因為后繼在阻塞前設置了它),它會找到其后繼節點(next
),并調用?LockSupport.unpark(successor.thread)
?喚醒其后繼線程。被喚醒的線程從?park()
?處返回,回到步驟 c 的循環中,再次嘗試獲取鎖。
-
-
入隊(
enq
)與設置前驅狀態(shouldParkAfterFailedAcquire
):-
入隊操作 (
enq
) 使用?CAS
?保證線程安全地更新?tail
。 -
shouldParkAfterFailedAcquire
?方法實現了步驟 c 中的邏輯:檢查/清理前驅狀態,確保前驅是有效的且?waitStatus == SIGNAL
,然后才決定調用?park()
。
-
7.5 與?synchronized
?關鍵字的對比
-
相似點:都是可重入互斥鎖。
-
ReentrantLock 優勢:
-
靈活性:支持公平鎖/非公平鎖選擇、可中斷鎖等待 (
lockInterruptibly
)、超時鎖等待 (tryLock(timeout)
)、多條件變量 (newCondition
)。 -
API 化:顯式的?
lock()
?和?unlock()
?操作,控制更精細。 -
性能:在高度競爭的場景下,現代 JVM 的?
synchronized
?優化(鎖升級)已經非常高效,兩者性能差距不大。但在某些特定場景(如大量讀少量寫),ReentrantLock 結合?ReadWriteLock
?可能更優。
-
-
synchronized 優勢:
-
簡潔性:語法簡單,由 JVM 自動管理鎖的獲取和釋放(在 synchronized 代碼塊結束時釋放),不易出錯(避免忘記?
unlock
)。 -
JVM 優化:JVM 深度優化(偏向鎖、輕量級鎖、鎖消除、鎖粗化等)。
-
下面說一下reentrantlock實現的
7.5 可中斷鎖和超時鎖
可中斷鎖:
源碼:
private void doAcquireInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE); // 將當前線程包裝為獨占模式Node加入隊列尾部boolean failed = true;try {for (;;) { // 自旋等待final Node p = node.predecessor(); // 獲取前驅節點if (p == head && tryAcquire(arg)) { // 如果前驅是頭節點(輪到我了)且嘗試獲取鎖成功setHead(node); // 將自己設為新的頭節點(出隊)p.next = null; // help GCfailed = false;return; // 成功獲取鎖,退出方法}if (shouldParkAfterFailedAcquire(p, node) && // 檢查是否應該阻塞(前驅狀態正常)parkAndCheckInterrupt()) // 4. 真正阻塞線程,并在此處檢查中斷!throw new InterruptedException(); // 5. 如果阻塞中被中斷,拋出異常!}} finally {if (failed)cancelAcquire(node); // 6. 如果最終失敗(如因中斷),取消節點獲取狀態}
}private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 使用LockSupport.park()掛起當前線程return Thread.interrupted(); // 線程被喚醒后,立即檢查并清除中斷標志,返回是否因中斷被喚醒
}
當線程判斷需要阻塞時(通過?shouldParkAfterFailedAcquire
),調用?LockSupport.park(this)
?掛起當前線程
-
線程可能被以下三種方式喚醒:
-
持有鎖的線程釋放鎖,并喚醒隊列中的后繼節點(
unparkSuccessor
)。 -
其他線程調用了該線程的?
interrupt()
?方法。 -
虛假喚醒(
spurious wakeup
)。
-
-
線程被喚醒后,第一件事就是調用?
Thread.interrupted()
。這個方法做兩件事:-
返回線程當前的中斷狀態(
true
?表示被中斷過)。 -
清除線程的中斷狀態(設為?
false
)。
-
超時鎖:
final boolean doAcquireNanos(int arg, long nanosTimeout) {if (nanosTimeout <= 0L) return false; // 時間已到,直接失敗final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE); // 加入隊列boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) { // 如果是隊首則嘗試獲取鎖setHead(node);p.next = null; // 幫助GCfailed = false;return true;}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L) return false; // 超時檢查if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { // 大于閾值才掛起LockSupport.parkNanos(this, nanosTimeout); // 關鍵:掛起指定時間}if (Thread.interrupted()) // 響應中斷throw new InterruptedException();}} finally {if (failed) cancelAcquire(node); // 失敗則取消節點}
}
其實可以看到 無論是可中斷鎖還是超時鎖,它們都使用了LockSupport這個對象來加鎖解鎖,沒有采用synchronized這種操作,而LockSupport支持鎖中斷,支持鎖超時機制,所以這就是reetrantlock能實現這些多功能鎖的原因了
7.6 Condition
基本使用:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();// 等待方
lock.lock();
try {while (!conditionSatisfied) { // 循環檢查條件condition.await(); // 釋放鎖并等待}// 執行條件滿足后的操作
} finally {lock.unlock();
}// 通知方
lock.lock();
try {// 改變條件condition.signal(); // 或 signalAll()
} finally {lock.unlock();
}
實現原理簡要介紹:
Condition
?的核心實現基于?AQS(AbstractQueuedSynchronizer),講解Condition時會涉及兩個隊列,一個是同步隊列,就是AQS存儲線程的隊列,一個是條件隊列,指的是condition對象對應的隊列,以下是關鍵原理:
-
等待隊列(條件隊列)
-
每個?
Condition
?對象內部維護一個?FIFO 等待隊列(單向鏈表)。 -
當線程調用?
await()
?時,會釋放鎖、修改對應lock鎖的狀態、并進入條件隊列等待。 -
-
隊列節點類型與 AQS 同步隊列相同(
Node
?類)。
-
-
節點轉移機制
-
調用?
signal()
?時,將條件隊列的頭節點移動到 AQS 同步隊列尾部。正好對應上了上面代碼的isOnSyncQueue -
-
移動到同步隊列的節點會嘗試獲取鎖,成功后從?
await()
?返回。
-
-
喚醒與阻塞控制
-
await()
:釋放鎖 → 阻塞線程 → 加入條件隊列。 -
signal()
:將條件隊列的首節點移入同步隊列,喚醒線程。 -
signalAll()
:移動條件隊列所有節點到同步隊列。
-
-
避免虛假喚醒
通過循環檢查條件(while (condition) await()
)確保線程被喚醒時條件真正滿足。
八、線程阻塞或喚醒時操作系統的動作
1.?用戶態嘗試獲取鎖
-
步驟:
-
線程在用戶態通過 CAS(Compare-And-Swap)自旋嘗試獲取鎖(例如?
synchronized
?的偏向鎖/輕量級鎖)。 -
若鎖未被競爭(無沖突),直接獲取成功,全程在用戶態完成。
-
-
關鍵點:
此時無系統調用,不涉及內核態切換,性能極高。
2.?競爭失敗:進入自適應自旋
-
步驟:
-
當 CAS 失敗(鎖被其他線程占用),JVM 啟動自適應自旋(根據歷史成功率動態調整自旋次數)。
-
線程在用戶態循環重試,仍不切內核態。
-
-
關鍵點:
自旋避免了立即陷入內核,但消耗 CPU 時間。
3.?自旋失敗:真正阻塞(切內核態)
-
步驟:
-
自旋仍無法獲取鎖時,JVM 調用底層操作系統的線程阻塞原語(如 Linux 的?
futex()
)。 -
線程狀態從?
RUNNABLE
?變為?BLOCKED
。 -
觸發系統調用(如?
futex
)→ 從用戶態陷入內核態。
-
-
關鍵點:
此處是用戶態切內核態的核心節點!
4.?內核態操作
-
內核完成以下動作:
-
保存線程上下文(寄存器、程序計數器等)。
-
將線程移入鎖的等待隊列(由內核管理)。
-
觸發線程調度:從就緒隊列選擇新線程運行。
-
切換 CPU 上下文到新線程。
-
-
關鍵點:
所有操作均在內核態完成,需要特權指令。
5.?喚醒階段(再次切內核態)
-
當鎖釋放時:
-
持有鎖的線程調用?
unlock()
,觸發 JVM 的喚醒機制。 -
JVM 通過?
futex_wake()
?系統調用陷入內核態。 -
內核從等待隊列中移出一個/多個線程,標記為就緒狀態。
-
線程重新參與調度,下次被 CPU 選中時恢復執行。
-
-
關鍵點:
喚醒操作同樣需切內核態,恢復線程時需切換回用戶態。
一點操作系統知識,大概了解一下:
上面使用到了futex,操作系統將所有等待同一 futex(相同 uaddr)的線程被組織在同一個等待隊列中
1.?uaddr 的本質
-
uaddr?是用戶空間的一個內存地址(32位整數),通常指向:
-
鎖狀態變量(如 Java 對象頭中的 MarkWord)
-
條件變量標志位
-
信號量計數器
-
-
關鍵特性:
相同 uaddr 值 → 代表同一同步資源(如同一把鎖)
-
一對一映射:
每個唯一的 uaddr 值對應?一個且僅一個?內核等待隊列 -
自動創建:
當首個線程在某個 uaddr 上調用?futex_wait()
?時,內核自動創建隊列 -
自動銷毀:
當隊列為空(無等待線程)時,內核自動銷毀隊列
// 每個 futex 等待隊列
struct futex_queue {struct list_head threads; // 線程鏈表(FIFO)u32 *uaddr; // 綁定的用戶態地址atomic_t waiters; // 等待線程計數
};
九、原子性、有序性、可見性的保證
特性 | 保障方法 |
---|---|
原子性 | synchronized 、ReentrantLock 、原子類(AtomicInteger ?等)、CAS |
可見性 | volatile 、synchronized /鎖、final 、CAS |
有序性 | volatile (禁止重排序)、synchronized /鎖(內存屏障)、fina 、CAS |