在黑馬點評項目實戰中,提到了可重入鎖,然后我想到了是不是不同業務在同一線程內反復獲取同一把鎖。本文來討論一下為什么鎖需要可重入。
一、可重入鎖的核心:“同一線程多次獲取同一把鎖”
??可重入(Reentrant)?? 的字面意思是“允許重新進入”,在鎖的上下文中,特指??同一線程可以多次獲取同一把鎖而不會被阻塞??。這與“不同業務”無直接關聯,而是針對??同一線程內的嵌套調用或遞歸調用??場景設計的。
1. 可重入的典型場景:同一線程的嵌套調用
假設你有一個遞歸方法或嵌套調用的業務邏輯,例如:
public void methodA() {lock.lock(); // 第一次加鎖try {// 業務邏輯1...methodB(); // 調用methodB} finally {lock.unlock(); // 最終釋放鎖(需確保只釋放一次)}
}public void methodB() {lock.lock(); // 嵌套調用,需要再次加鎖try {// 業務邏輯2...} finally {lock.unlock();}
}
如果鎖是??不可重入??的,當methodA
獲取鎖后調用methodB
時,methodB
嘗試再次加鎖會被阻塞(因為鎖已被當前線程持有),導致死鎖。而??可重入鎖??允許同一線程多次加鎖(每次加鎖重入次數+1),直到所有加鎖操作都被釋放(重入次數減至0),從而避免死鎖。
2. 與“不同業務”的區別
這里的“不同業務”并非指不同線程或不同功能模塊,而是??同一線程內的不同代碼路徑??(如遞歸、循環調用)。例如:
- 電商下單場景中,主線程先校驗庫存(調用
checkStock()
),再扣減庫存(調用deductStock()
),兩個方法都需要同一把鎖。若鎖不可重入,checkStock()
加鎖后,deductStock()
會因無法獲取鎖而阻塞;可重入鎖則允許checkStock()
加鎖后,deductStock()
直接獲取已持有的鎖(重入次數+1)。
二、為什么需要可重入鎖?應對復雜業務的“嵌套鎖需求”
可重入鎖的設計主要是為了??簡化復雜業務邏輯中的鎖管理??。在真實業務中,嵌套調用(如方法A調用方法B,兩者都需要同一把鎖)非常常見,若使用不可重入鎖,開發者需手動維護鎖的獲取次數(例如,通過計數器記錄嵌套層級),否則容易因忘記釋放鎖或重復釋放導致死鎖或數據不一致。
1. 不可重入鎖的痛點
假設使用不可重入鎖,開發者需手動處理嵌套調用的鎖邏輯:
public class NonReentrantLockExample {private int lockCount = 0; // 手動維護重入次數private final Object lock = new Object();public void methodA() {synchronized (lock) { // 不可重入鎖,第一次加鎖lockCount++;try {// 業務邏輯...methodB(); // 調用methodB} finally {lockCount--;if (lockCount == 0) {// 手動釋放鎖(僅當重入次數為0時)}}}}public void methodB() {synchronized (lock) { // 不可重入鎖,此處會阻塞!// 業務邏輯...}}
}
這種情況下,methodB
的synchronized
塊會因無法獲取鎖而永久阻塞,必須通過復雜的計數器邏輯手動管理鎖的釋放,容易出錯。
2. 可重入鎖的簡化
Redisson的可重入鎖通過??自動維護重入次數??解決了這一問題:
- 當同一線程首次獲取鎖時,重入次數初始化為1;
- 若同一線程再次獲取同一把鎖(如嵌套調用),重入次數遞增(如2、3...);
- 釋放鎖時,重入次數遞減,直到次數為0時才真正釋放鎖(通知Redis刪除鎖)。
開發者無需手動維護重入次數,鎖的獲取和釋放邏輯與普通單次加鎖一致,大大降低了復雜度。
三、鎖值(重入次數)的含義:“當前線程持有鎖的次數”
Redisson的可重入鎖在Redis中存儲的鍵值對結構大致如下(通過RedissonLock
類的tryLock
方法實現的Lua腳本):
-- 鎖的鍵:lockKey(如"lock:user:123")
-- 鎖的值:持有者ID(如線程ID+客戶端ID) + 重入次數(初始為1)
if not redis.call('exists', KEYS[1]) thenredis.call('hset', KEYS[1], ARGV[2], 1) -- 持有者ID -> 重入次數1redis.call('pexpire', KEYS[1], ARGV[1]) -- 設置超時時間return 1
elseif redis.call('hexists', KEYS[1], ARGV[2]) == 1 thenlocal count = redis.call('hincrby', KEYS[1], ARGV[2], 1) -- 重入次數+1redis.call('pexpire', KEYS[1], ARGV[1]) -- 重置超時時間(防止過期)return nil
elsereturn 0
end
其中,??鎖的值(存儲在Redis的Hash結構中)?? 包含兩部分:
- ??持有者ID??:唯一標識當前持有鎖的線程(如
thread:123@client:456
,避免不同節點的線程ID沖突); - ??重入次數??:當前線程已獲取該鎖的次數(初始為1,每次重入+1,釋放時-1)。
1. 鎖值≠0的含義
當鎖值(重入次數)??大于0??時,說明??當前線程仍持有該鎖??(可能是在嵌套調用中,或尚未釋放所有重入次數)。此時,其他線程嘗試獲取該鎖會被阻塞(直到鎖超時或當前線程釋放)。
2. 鎖值=0的含義
當鎖值??等于0??時,說明當前線程已完全釋放該鎖(所有重入次數已耗盡),Redis會刪除該鎖的鍵,其他線程可以重新競爭獲取。
四、總結:可重入鎖的本質是“同一線程的鎖重用”
Redisson可重入鎖的“可重入”核心是??允許同一線程多次獲取同一把鎖??,解決的是復雜業務中嵌套調用(如遞歸、方法調用鏈)導致的鎖沖突問題。其本質是“同一線程的鎖重用”,而非“不同業務的鎖共享”。
鎖值(重入次數)記錄了當前線程持有鎖的次數,只有當次數減至0時,鎖才真正釋放。這一設計避免了開發者手動維護嵌套鎖的復雜性,是分布式系統中處理復雜業務邏輯的重要工具。