鎖概述
在計算機科學中,鎖是在執行多執行緒時用于強行限制資源訪問的同步機制,即用于在并發控制中保證對互斥要求的滿足,
鎖相關概念
- 鎖開銷:完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間,
- 鎖競爭:一個執行緒或行程,要獲取另一個執行緒或行程所持有的鎖,邊會發生鎖競爭,鎖粒度越小,競爭的可能越小,
- 死鎖:多個執行緒爭奪資源互相等待資源釋放導致阻塞;由于無限期阻塞,程式不能正常終止,
分類
- 樂觀鎖、悲觀鎖:是否鎖定同步資源,
- 樂觀鎖:認為其他執行緒對資料訪問時 不會 修改資料,實際未加鎖,更新資料時判斷是否被其他執行緒更新了(讀時不加鎖,寫時加鎖),
- 適合多讀的場景,因為讀操作沒有加鎖,
- 實作原理: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
- 分段鎖:將要鎖定的資料拆分成段后對所需資料段加鎖,減少鎖定范圍
相關閱讀:
- Java中的鎖 - 沈三白
- 聽說你知道什么是鎖 --JAVA - 羅小扇
自定義鎖工具
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,多次加解鎖會導致與預期不符)
- 不可重入
-
參考:https://github.com/realpdai/tech-pdai-spring-demos/blob/main/264-springboot-demo-redis-jedis-distribute-lock/src/main/java/tech/pdai/springboot/redis/jedis/lock/lock/RedisDistributedLock.java
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/539091.html
標籤:Java
