鎖概述 在電腦科學中,鎖是在執行多線程時用於強行限制資源訪問的同步機制,即用於在併發控制中保證對互斥要求的滿足。 鎖相關概念 鎖開銷:完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間。 鎖競爭:一個線程或進程,要獲取另一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能 ...
鎖概述
在電腦科學中,鎖是在執行多線程時用於強行限制資源訪問的同步機制,即用於在併發控制中保證對互斥要求的滿足。
鎖相關概念
- 鎖開銷:完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間。
- 鎖競爭:一個線程或進程,要獲取另一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能越小。
- 死鎖:多個線程爭奪資源互相等待資源釋放導致阻塞;由於無限期阻塞,程式不能正常終止。
分類
- 樂觀鎖、悲觀鎖:是否鎖定同步資源。
- 樂觀鎖:認為其他線程對數據訪問時 不會 修改數據,實際未加鎖,更新數據時判斷是否被其他線程更新了(讀時不加鎖,寫時加鎖)。
- 適合多讀的場景,因為讀操作沒有加鎖。
- 實現原理:CAS (compare-and-swap) ,無鎖演算法,原子操作比較更新。
- 使用:
- Java 中的 CAS 鎖(AtomicXxx)通過 JNI 調用 CPU 中的 cmpxchg 彙編指令實現
- 資料庫表增加 version 欄位,更新時判斷 version 未改變。
- 缺陷:
- ABA 問題:數據發生類似變化(A -> B -> A),會認為數據沒有改變。
JDK 1.5 引入 AtomicStampedReference 增加標誌位(1A -> 2B -> 3A) - 自旋問題:CAS 無法獲取到鎖會在超時時間內迴圈獲取,造成 CPU 資源浪費
- ABA 問題:數據發生類似變化(A -> B -> A),會認為數據沒有改變。
- 悲觀鎖:認為其他線程對數據訪問時 一定會 修改數據,訪問數據時加鎖同步處理(一開始加鎖無論讀寫)。
- 適合多寫的場景,獨占數據的讀寫許可權,確保數據的讀取和更新都是準確的。
- 樂觀鎖:認為其他線程對數據訪問時 不會 修改數據,實際未加鎖,更新數據時判斷是否被其他線程更新了(讀時不加鎖,寫時加鎖)。
- 讀寫鎖
- 讀鎖:共用鎖,可支持多線程併發讀。
- 寫鎖:獨享鎖,讀寫、寫寫互斥。
- 示例:ReentrantReadWriteLock
- 可重入鎖、不可重入鎖
- 可重入鎖(遞歸鎖):一個線程在已加鎖範圍內代碼中再次進行加鎖能夠獲取到鎖
- synchronized 、 ReentrantLock
- 不可重入鎖:一個線程對在已加鎖範圍內代碼中再次進行加鎖操作,由於第二次加鎖時需要等待上次鎖釋放才可以加鎖造成鎖的互相等待
- 可重入鎖(遞歸鎖):一個線程在已加鎖範圍內代碼中再次進行加鎖能夠獲取到鎖
- 公平鎖、非公平鎖
- 公平鎖:多個線程按照申請鎖的順序來獲取鎖,依賴 AQS 隊列,線程直接進入隊列中排隊,第一個線程才能獲取到鎖
- 非公平鎖:多個線程加鎖時嘗試直接獲取鎖,獲取不到進入隊列,可能出現後申請鎖的線程先獲取到鎖
- 優點:可以減少喚起線程的開銷,整體吞吐效率高
- 缺點:處於等待隊列中的線程可能餓死
- synchronized
- 示例:ReentrantLock 預設為非公平鎖,構造方法可指定為公平鎖
new ReentrantLock(true);
- 偏向鎖、輕量鎖、重量鎖:synchronized 的三種鎖狀態。
- 偏向鎖:鎖標誌位 101,在對象頭(Mark Word)和棧幀中鎖記錄(Lock Record)里存儲線程ID,通過 對比 Mark Word 避免執行 CAS
- JDK 6 引入,JDK 15 標記廢棄,可通過 JVM 參數(-XX:+UseBiasedLocking)手動啟用
- 輕量鎖:鎖標誌位 000,偏向鎖時出現競爭升級為輕量鎖,未獲取到鎖的線程自旋獲取,通過 CAS + 自旋 避免線程阻塞喚醒
- 重量鎖:鎖標誌位 010,輕量鎖自旋超過一定此處升級為重量鎖,未獲取到鎖的線程休眠
- 偏向鎖:鎖標誌位 101,在對象頭(Mark Word)和棧幀中鎖記錄(Lock Record)里存儲線程ID,通過 對比 Mark Word 避免執行 CAS
- 分段鎖、自旋鎖:鎖設計,非特定的鎖。
- 分段鎖:將要鎖定的數據拆分成段後對所需數據段加鎖,減少鎖定範圍
- ConcurrentHashMap 在 JDK 8 之前使用 Segment (繼承 ReentrantLock)對桶數組分割分段加鎖
- 自旋鎖:試探獲取資源,未獲取到採取自旋迴圈
where(true)
再次試探獲取,不阻塞線程- 輕量鎖通過 CAS + 自旋 實現
- 優點:減少上下文切換
- 缺點:占用 CPU
- 分段鎖:將要鎖定的數據拆分成段後對所需數據段加鎖,減少鎖定範圍
相關閱讀:
自定義鎖工具
1 :Redis 分散式鎖(簡單實現)
使用 ThreadLocal 保存鎖對應的唯一標識
加鎖:使用 STRING 保存鎖定標識, 'SET key value PX NX' 確保一個 key 只能加鎖一次
解鎖:Lua 腳本判斷是自己加的鎖進行釋放
-
工具類
RedisSimpleLockUtil.java
// 使用 ThreadLocal 保存鎖對應的唯一標識 private static final ThreadLocal<String> LOCK_FLAG = ThreadLocal.withInitial(() -> UUID.randomUUID().toString().replace("-", "").toLowerCase() ); // 嘗試加鎖 private boolean tryLock(String key, long ttl) { try { String val = LOCK_FLAG.get(); Boolean lockRes = redisTemplate.opsForValue() .setIfAbsent(key, val, ttl, TimeUnit.MILLISECONDS); log.debug("tryLock, key={}, val={}, lockRes={}", key, val, lockRes); return Boolean.TRUE.equals(lockRes); } catch (Exception e) { log.error("tryLock occurred an exception", e); } return false; } // 解鎖 public boolean unlock(String key) { boolean succeed = false; try { List<String> keys = Collections.singletonList(key); Object[] args = {LOCK_FLAG.get()}; Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args); log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes); succeed = Optional.ofNullable(unlockRes).filter(res -> res > 0).isPresent(); } catch (Exception e) { log.error("unlock occurred an exception", e); } finally { if (succeed) { LOCK_FLAG.remove(); } } return succeed; }
-
Lua 腳本
解鎖: redis_unlock_simple.lua
local lock_key = KEYS[1]; local lock_flag = ARGV[1]; --- 判斷鎖定的唯一標識與參數一致刪除鎖 --- 返回值:1=解鎖成功(刪除成功),0=鎖已失效或刪除失敗,-1=非自己的鎖不支持解鎖 local val = redis.call('GET', lock_key); if (not val) then return 0; elseif (val == lock_flag) then return redis.call('DEL', lock_key); else return -1; end
-
缺陷
- 只能單次加鎖(唯一標識通過 ThreadLocal 存儲,解鎖時會清理 ThreadLocal,多次加解鎖會導致與預期不符)
- 不可重入
2 :Redis 分散式鎖
使用 ThreadLocal 保存 鎖key 與 相應的唯一標識
加鎖:使用 HASH 保存鎖標識與加鎖次數
解鎖:Lua 腳本判斷是自己加的鎖進行釋放
功能:可重入(Redis HASH)、支持對不同 key 進行加解鎖(ThreadLocal<Map<String, String>>)
-
工具類
RedisLockUtil.java
// 使用 ThreadLocal 保存 鎖key 與 唯一標識 private static final ThreadLocal<Map<String, String>> LOCK_FLAG = ThreadLocal.withInitial(HashMap::new); // 嘗試加鎖 private long tryLock(String key, long ttl) { String uniqueFlag = LOCK_FLAG.get().get(key); if (uniqueFlag == null) { uniqueFlag = UUID.randomUUID().toString().replace("-", ""); LOCK_FLAG.get().put(key, uniqueFlag); } try { List<String> keys = Collections.singletonList(key); Object[] args = {uniqueFlag, ttl}; Long lockRes = redisTemplate.execute(LOCK_SCRIPT, keys, args); log.debug("tryLock, lock_flag={}, key={}, args={}, lockRes={}", LOCK_FLAG.get(), key, args, lockRes); return lockRes != null ? lockRes : 0L; } catch (Exception e) { log.error("tryLock occurred an exception", e); } return 0L; } // 嘗試解鎖 public long tryUnlock(String key) { String uniqueFlag = LOCK_FLAG.get().get(key); if (uniqueFlag == null) { return 0L; } long lockNum = -1L; try { List<String> keys = Collections.singletonList(key); Object[] args = {uniqueFlag}; Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args); log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes); lockNum = unlockRes != null ? unlockRes : 0L; } catch (Exception e) { log.error("release lock occurred an exception", e); } finally { if (lockNum == 0L) { LOCK_FLAG.get().remove(key); if (LOCK_FLAG.get().isEmpty()) { LOCK_FLAG.remove(); } } } return lockNum; }
-
Lua 腳本
加鎖: redis_lock.lua
```lua local lock_key = KEYS[1]; local lock_flag = ARGV[1]; --- 鎖定時長,單位:毫秒 local lock_ttl = tonumber(ARGV[2]); --- HASH 支持可重入 --- lock_flag 保存加鎖唯一標識 --- lock_num 保存加鎖次數 local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num"); local h_flag = info[1]; local h_num = tonumber(info[2]); if (h_num == nil or h_num < 0) then h_num = 0; end --- 返回加鎖次數,未加鎖成功返回 -1 if (not h_flag or h_flag == lock_flag) then local res_num = h_num + 1; redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num); redis.call("PEXPIRE", lock_key, lock_ttl); return res_num; else return -1; end ```
解鎖: redis_unlock.lua
```lua local lock_key = KEYS[1]; local lock_flag = ARGV[1]; --- HASH 支持可重入 --- lock_flag 保存加鎖唯一標識 --- lock_num 保存加鎖次數 local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num"); local h_flag = info[1]; local h_num = tonumber(info[2]); if (h_num == nil) then h_num = 0; end --- 返回剩餘加鎖次數,未被加鎖或解鎖完返回 0,非自己加鎖返回 -1 if (not h_flag) then return 0; elseif (h_flag == lock_flag) then if (h_num <= 0) then redis.call("DEL", lock_key); return 0; else local res_num = h_num - 1; redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num); return res_num; end else return -1; end ```
其他
demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-lock
作者:EastX本文發自博客園,歡迎轉載,轉載請註明原文鏈接:https://www.cnblogs.com/cnx01/p/16948315.html