Java鎖機制完整學習筆記(附詳細代碼)
一、鎖的起源:為什么需要鎖?
1.1 問題場景
int count = 0; // 線程A 和 線程B 同時執(zhí)行 count++;
count++在CPU層面是三步操作:
1. 讀取 count 的值到寄存器
2. 寄存器的值 +1
3. 把結(jié)果寫回內(nèi)存
1.2 并發(fā)問題
線程A: 讀取count=0
線程B: 讀取count=0
線程A: 計算0+1=1
線程B: 計算0+1=1
線程A: 寫入count=1
線程B: 寫入count=1結(jié)果:count=1,而不是期望的2
核心問題:如何讓一組操作不可分割地執(zhí)行?
二、volatile 能解決嗎?
2.1 答案:不能
volatile int count = 0; // 依然有問題 count++;
2.2 volatile 解決的是
| 特性 | 說明 |
|---|---|
| 可見性 | 一個線程修改后,其他線程立即看到 |
| 有序性 | 禁止指令重排序 |
不解決原子性 —— "讀-改-寫"之間,別的線程還是可以插進來。
三、CAS:硬件級別的原子操作
3.1 CPU 提供的能力
CPU 提供特殊指令(x86 的 CMPXCHG):
比較內(nèi)存值是否等于預(yù)期值
如果相等,就把新值寫入
整個過程不可被打斷
這就是 Compare-And-Swap。
3.2 技術(shù)棧關(guān)系
CPU 提供:CAS指令(硬件能力,一直都有)
↓
JVM 實現(xiàn):Unsafe類(Java訪問底層能力的橋梁)
↓
JDK 封裝:LockSupport、Atomic類等
四、Java 鎖的演進歷程
4.1 演進時間線
JDK 1.0:synchronized(重量級鎖)
JDK 1.5:ReentrantLock、CAS、AQS
JDK 1.6:鎖升級(偏向鎖、輕量級鎖)
JDK 1.8:StampedLock(樂觀讀)
JDK 15: 廢棄偏向鎖
JDK 21: 虛擬線程,synchronized 有 Pinning 問題
JDK 24+:修復(fù) synchronized 支持虛擬線程
五、JDK 1.0:synchronized 重量級鎖
5.1 實現(xiàn)方式
synchronized (obj) {
count++;
}
早期實現(xiàn):
- 直接調(diào)用操作系統(tǒng)的互斥量(Mutex)
- 需要從用戶態(tài)切換到內(nèi)核態(tài)
- 讓搶不到鎖的線程進入阻塞狀態(tài)
5.2 內(nèi)核態(tài)切換的開銷
為什么需要切換?
用戶態(tài)(User Mode):Java程序運行的地方,權(quán)限受限
內(nèi)核態(tài)(Kernel Mode):操作系統(tǒng)運行的地方,權(quán)限最高
阻塞線程、喚醒線程等操作需要操作系統(tǒng)介入,必須切換。
切換時發(fā)生了什么?
1. 保存現(xiàn)場
- 當前所有CPU寄存器的值
- 程序計數(shù)器(執(zhí)行到哪了)
- 棧指針
→ 全部存到內(nèi)存2. 切換棧
- 從用戶棧切到內(nèi)核棧3. 執(zhí)行內(nèi)核代碼
- 內(nèi)核的代碼和數(shù)據(jù)被加載到CPU緩存
- 程序的代碼和數(shù)據(jù)可能被擠出緩存4. 返回用戶態(tài)
- 恢復(fù)之前保存的所有寄存器
- 切回用戶棧
- 重新加載程序的代碼和數(shù)據(jù)到緩存
開銷量化
CPU緩存訪問:約 1-10 納秒
內(nèi)存訪問:約 100 納秒一次 count++: 約 幾個 CPU周期
一次內(nèi)核態(tài)切換: 約 幾千~幾萬個 CPU周期
問題:鎖本身的開銷 >> 臨界區(qū)代碼的開銷
六、JDK 1.5:ReentrantLock 與 AQS
6.1 改進思路
先用 CAS 自旋幾次(用戶態(tài),不切換)
↓
還拿不到?再調(diào)用 park 阻塞(才進內(nèi)核態(tài))
能不阻塞就不阻塞。
6.2 對比
早期 synchronized:
拿不到 → 立即阻塞 → 進內(nèi)核態(tài)ReentrantLock:
拿不到 → 先自旋 → 還不行再阻塞
| 特性 | Thread.sleep() | Object.wait() | LockSupport.park() |
|---|---|---|---|
| 鎖釋放 | 不釋放任何鎖 | 釋放 synchronized 監(jiān)視器鎖 | 不直接操作鎖 |
| 喚醒方式 | 超時自動喚醒 | notify()/notifyAll() | unpark(Thread) |
| 喚醒目標 | 只能喚醒自己 | 隨機或全部 | 精確喚醒指定線程 |
| 使用位置 | 任何地方 | 必須在 synchronized 塊內(nèi) | 任何地方 |
6.4 AQS 核心結(jié)構(gòu)
AQS = state 狀態(tài)變量 + CLH 雙向隊列
CLH:Craig, Landin, and Hagersten Queue,三個發(fā)明者的名字。
七、JDK 1.6:synchronized 鎖升級
7.1 核心思想
根據(jù)競爭激烈程度,逐步升級:
無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖
7.2 對象頭 Mark Word
每個 Java 對象在內(nèi)存中都有對象頭,其中 Mark Word(64位)存儲鎖信息:
鎖標志位(最后2位):
01 → 無鎖 或 偏向鎖(看倒數(shù)第3位區(qū)分)
00 → 輕量級鎖
10 → 重量級鎖詳細結(jié)構(gòu):
┌─────────────────────────────────────────────────┐
│ 無鎖: │ hashCode │ 分代年齡 │ 0 │ 01 │ │
│ 偏向鎖: │ 線程ID │ epoch │ 分代年齡 │ 1 │ 01 │ │
│ 輕量級: │ 指向棧中鎖記錄的指針 │ 00 │ │
│ 重量級: │ 指向Monitor對象的指針 │ 10 │ │
└─────────────────────────────────────────────────┘
7.3 偏向鎖
場景:只有一個線程反復(fù)訪問
// 線程A 第一次進入
synchronized (lock) {
count++;
}
// 對象頭寫入線程A的ID
// 線程A 再次進入
synchronized (lock) {
count++;
}
// 檢查線程ID == A?直接進入,零開銷
特點:
- 加鎖:比較線程ID,匹配就進入
- 解鎖:什么都不做,對象頭保持不變
7.4 輕量級鎖
場景:多個線程交替訪問,無激烈競爭
觸發(fā)條件: 第二個線程來訪問時
// 線程A 進入
synchronized (lock) {
count++;
}
// CAS 修改對象頭,指向A的鎖記錄
// 線程A 離開
// CAS 恢復(fù)對象頭,還原原來的Mark Word
特點:
- 每次都要執(zhí)行 CAS 加鎖和解鎖
- 開銷比偏向鎖大,但仍在用戶態(tài)
7.5 重量級鎖
場景:CAS 自旋多次仍失敗,競爭激烈
Monitor 結(jié)構(gòu):
┌─────────────────────────┐ │ Owner: 當前持有鎖的線程 │ │ EntryList: 阻塞等待的線程 │ ← 搶鎖失敗的線程排隊 │ WaitSet: 調(diào)用wait()的線程 │ └─────────────────────────┘
7.6 鎖升級流程圖
lock 對象剛創(chuàng)建
│
▼
無鎖(0 01)
│
│ 線程A第一次訪問
▼
偏向鎖(1 01)── 對象頭記錄線程A的ID
│
│ 線程B來訪問
▼
輕量級鎖(00)── CAS自旋競爭
│
│ 自旋失敗,競爭激烈
▼
重量級鎖(10)── 阻塞等待,進內(nèi)核態(tài)
7.7 三種鎖的開銷對比
偏向鎖: 比較ID → 進入 (一次比較)
輕量級鎖:CAS修改指針 → 進入 (一次原子操作)
重量級鎖:系統(tǒng)調(diào)用 → 可能阻塞 (內(nèi)核態(tài)切換)
7.8 鎖升級的特點
1. 單向升級,不會降級
2. 一旦出現(xiàn)過競爭,JVM認為后續(xù)還可能有競爭
八、JDK 15:廢棄偏向鎖
8.1 原因
現(xiàn)代應(yīng)用并發(fā)程度高,單線程訪問場景少
偏向鎖撤銷的開銷 > 它帶來的收益
8.2 變化
鎖升級變成:
無鎖 → 輕量級鎖 → 重量級鎖
跳過了偏向鎖
九、讀寫鎖
9.1 Java 讀寫鎖(ReentrantReadWriteLock)
存儲位置:AQS 的 state 變量(32位 int)
┌─────────────────┬─────────────────┐
│ 高16位:讀鎖數(shù)量 │ 低16位:寫鎖數(shù)量 │
└─────────────────┴─────────────────┘
9.2 Java 與數(shù)據(jù)庫鎖對應(yīng)關(guān)系
| Java | 數(shù)據(jù)庫 |
|---|---|
| 讀鎖(Read Lock) | 共享鎖(S鎖) |
| 寫鎖(Write Lock) | 排他鎖(X鎖) |
9.3 兼容矩陣
讀鎖 寫鎖
讀鎖 兼容 ? 沖突 ?
寫鎖 沖突 ? 沖突 ?
核心:讀讀不沖突,其他都沖突
十、數(shù)據(jù)庫鎖
10.1 意向鎖(Intention Lock)
解決的問題:
事務(wù)A:鎖住某一行(行級X鎖)
事務(wù)B:想鎖整張表(表級X鎖)
↓
事務(wù)B 需要確保表里沒有任何行被鎖住
↓
沒有意向鎖:遍歷100萬行檢查
有意向鎖:檢查表級標記,直接判斷
工作流程:
事務(wù)A:
1. 先給表加 IX鎖(意向排他鎖)
2. 再給行加 X鎖事務(wù)B 想加表鎖:
1. 檢查表級別有沒有意向鎖
2. 發(fā)現(xiàn)有 IX鎖 → 直接等待
意向鎖兼容矩陣:
IS IX S X IS ? ? ? ? IX ? ? ? ? S ? ? ? ? X ? ? ? ?
10.2 粒度鎖
解決的問題:幻讀
-- 表里有 id = 1, 5, 10 三行 -- 事務(wù)A SELECT * FROM users WHERE id > 3 AND id < 8; -- 結(jié)果:id = 5 -- 事務(wù)B 插入新行 INSERT INTO users (id) VALUES (6); COMMIT; -- 事務(wù)A 再查一次 SELECT * FROM users WHERE id > 3 AND id < 8; -- 結(jié)果:id = 5, 6 ← 幻讀
三種鎖:
| 鎖類型 | 說明 | 作用 |
|---|---|---|
| 記錄鎖(Record Lock) | 鎖具體的行 | 防止修改/刪除 |
| 間隙鎖(Gap Lock) | 鎖行之間的空隙 | 防止插入 |
| 臨鍵鎖(Next-Key Lock) | 記錄鎖 + 間隙鎖 | 防止幻讀 |
間隙示例:
表里有 id = 1, 5, 10
間隙:(-∞, 1) (1, 5) (5, 10) (10, +∞)
查詢 id > 3 AND id < 8 時:
鎖住間隙 (1, 5) 和 (5, 10)
↓
插入 id = 6 被阻塞
十一、StampedLock(JDK 1.8)
11.1 解決的問題
ReentrantReadWriteLock 問題:
1. 讀鎖和寫鎖互斥,大量讀時寫線程饑餓
2. 即使只是讀,也要加鎖
11.2 樂觀讀
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 獲取版本號,不加鎖
int x = this.x; // 讀數(shù)據(jù)
int y = this.y;
if (!lock.validate(stamp)) { // 驗證版本號
// 版本變了,升級為悲觀讀鎖
stamp = lock.readLock();
try {
x = this.x;
y = this.y;
} finally {
lock.unlockRead(stamp);
}
}
11.3 三種模式
| 模式 | 方法 | 說明 |
|---|---|---|
| 樂觀讀 | tryOptimisticRead() | 不加鎖,性能最好 |
| 悲觀讀 | readLock() | 和讀寫鎖一樣 |
| 寫鎖 | writeLock() | 排他 |
11.4 對比
| ReentrantReadWriteLock | StampedLock | |
|---|---|---|
| 樂觀讀 | ? | ? |
| 可重入 | ? | ? |
| 支持Condition | ? | ? |
十二、公平鎖 vs 非公平鎖
| 類型 | 說明 | 性能 |
|---|---|---|
| 公平鎖 | 嚴格按隊列順序,不允許插隊 | 較低 |
| 非公平鎖 | 允許插隊,誰搶到誰用 | 較高 |
ReentrantLock 默認是非公平的。
十三、可重入鎖
概念: 同一線程可以多次獲取同一把鎖
實現(xiàn)原理: 判斷鎖的持有者是否是當前線程
synchronized (lock) { // 第一次獲取
synchronized (lock) { // 同一線程再次獲取,允許
// ...
}
}
作用: 避免同一線程自己把自己鎖死
十四、JDK 21+:虛擬線程與 Pinning 問題
14.1 虛擬線程原理
平臺線程(OS線程):重量級,數(shù)量有限
虛擬線程:輕量級,可以創(chuàng)建百萬個┌─────────────────────────────────────┐
│ 平臺線程(載體線程) │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │虛擬1│ │虛擬2│ │虛擬3│ 輪流執(zhí)行 │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────┘
正常情況:
虛擬線程1 執(zhí)行中,遇到阻塞
↓
JVM 把虛擬線程1 從平臺線程上"卸載"
↓
平臺線程去執(zhí)行虛擬線程2
↓
虛擬線程1 的阻塞完成后,再"掛載"回來
14.2 Pinning 問題
synchronized (lock) {
httpClient.send(request); // 網(wǎng)絡(luò)IO阻塞
}
問題流程:
虛擬線程進入 synchronized 塊
↓
Monitor(監(jiān)視器)記錄在平臺線程的棧幀上
↓
遇到 IO 阻塞,想卸載虛擬線程
↓
但 Monitor 信息和平臺線程綁定,無法卸載
↓
虛擬線程"釘住"了平臺線程
↓
平臺線程只能傻等
14.3 為什么 ReentrantLock 沒這個問題?
ReentrantLock 用 Java 代碼實現(xiàn)(AQS)
↓
鎖狀態(tài)存在堆內(nèi)存的 state 變量中
↓
不依賴平臺線程的棧
↓
虛擬線程可以正常卸載
14.4 JDK 24 修復(fù)(JEP 491)
重新實現(xiàn) synchronized 的底層機制
↓
Monitor 信息從棧上移到堆上
↓
不再和平臺線程綁定
↓
虛擬線程可以正常卸載
對,你說得很準確。我更新一下總結(jié):
十五、總結(jié):鎖的本質(zhì)
兩種實現(xiàn)路徑
| 鎖類型 | 爭搶目標 | 存儲位置 |
|---|---|---|
| synchronized | 對象頭 Mark Word | 對象內(nèi)存布局 |
| ReentrantLock / StampedLock | state 狀態(tài)變量 | AQS 堆內(nèi)存 |
核心機制
synchronized:
CAS 修改對象頭 → 成功則拿到鎖JUC 鎖:
CAS 修改 state 變量 → 成功則拿到鎖
演進思路
能不阻塞就不阻塞
能在用戶態(tài)解決就不進內(nèi)核態(tài)
根據(jù)競爭程度選擇合適的鎖
總結(jié)
到此這篇關(guān)于Java鎖機制的文章就介紹到這了,更多相關(guān)Java鎖機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis-plus與mybatis共存的實現(xiàn)
本文主要介紹了mybatis-plus與mybatis共存的實現(xiàn),文中根據(jù)實例編碼詳細介紹的十分詳盡,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
java Quartz定時器任務(wù)與Spring task定時的幾種實現(xiàn)方法
本篇文章主要介紹了java Quartz定時器任務(wù)與Spring task定時的幾種實現(xiàn)方法的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02
springboot整合ehcache 實現(xiàn)支付超時限制的方法
在線支付系統(tǒng)需要極高的穩(wěn)定性,在有限的系統(tǒng)資源下,穩(wěn)定性優(yōu)先級要高于系統(tǒng)并發(fā)以及用戶體驗,因此需要合理的控制用戶的支付請求。下面通過本文給大家介紹springboot整合ehcache 實現(xiàn)支付超時限制的方法,一起看看吧2018-01-01

