Java 中通過 key 獲取鎖的方法
一、概覽
本文我們將了解如何通過特定鍵獲取鎖,以保證該鍵上的操作的線程安全,并且不妨礙其他鍵。
一般來說,我們需要實(shí)現(xiàn)兩個方法:
void lock(String key) void unlock(String key)
本文以字符串作為鍵為例,大家可以根據(jù)實(shí)際需要改造成任意類型的鍵,重寫 equas 和 hashCode 方法,保證唯一性即可。
二、簡單的互斥鎖
假設(shè)需要滿足當(dāng)前線程獲取鎖則需要執(zhí)行特定代碼,否則不執(zhí)行這個場景。
我們可以維護(hù)一系列 Key 的 Set, 在使用時添加到 Set 中,解鎖時移除對應(yīng)的 Key。
此時,需要考慮線程安全問題。因此需要使用線程安全的 Set 實(shí)現(xiàn),如基于 ConcurrentHashMap 的線程安全 Set。
public class SimpleExclusiveLockByKey {
private static Set<String> usedKeys= ConcurrentHashMap.newKeySet();
public boolean tryLock(String key) {
return usedKeys.add(key);
}
public void unlock(String key) {
usedKeys.remove(key);
}
}使用案例:
String key = "key";
SimpleExclusiveLockByKey lockByKey = new SimpleExclusiveLockByKey();
try {
lockByKey.tryLock(key);
// 在這里添加對該 key 獲取鎖之后要執(zhí)行的代碼
} finally { // 非常關(guān)鍵
lockByKey.unlock(key);
}注意一定要在 finally 代碼塊中解鎖,以保證即便發(fā)生異常時,也可以正常解鎖。
三、按鍵來獲取和釋放鎖
以上代碼可以保證獲取鎖后才執(zhí)行,但無法實(shí)現(xiàn)未拿到鎖的線程等待的效果。
有時候,我們需要讓未獲取到對應(yīng)鎖的線程等待。
流程如下:
- 第一個線程獲取某個 key 的鎖
- 第二個線程獲取同一個 key 的鎖,第二個線程需要等待
- 第一個線程釋放某個 key 的鎖
- 第二個線程獲取該 key 的鎖,然后執(zhí)行其代碼
3.1 使用線程計數(shù)器定義 Lock
我們可以使用 ReentrantLock 來實(shí)行線程阻塞。
我們通過內(nèi)部類來封裝 Lock。該類統(tǒng)計某個 key 上執(zhí)行的線程數(shù)。暴露兩個方法,一個是線程數(shù)增加,一個是減少線程數(shù)。
private static class LockWrapper {
private final Lock lock = new ReentrantLock();
private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
private LockWrapper addThreadInQueue() {
numberOfThreadsInQueue.incrementAndGet();
return this;
}
private int removeThreadFromQueue() {
return numberOfThreadsInQueue.decrementAndGet();
}
}3.2 處理排隊的線程
接下來繼續(xù)使用 ConcurrentHashMap , key 作為鍵, LockWrapper 作為值。
保證同一個 key 使用同一個 LockWrapper 中的同一把鎖。
private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
一個線程想要獲取某個 key 的鎖時,需要看該 key 對應(yīng)的 LockWrapper 是否已經(jīng)存在。
- 如果不存在,創(chuàng)建一個 LockWrapper ,計數(shù)器設(shè)置為1
- 如果存在,對應(yīng)的 LockWrapper 加1
public void lock(String key) {
LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
lockWrapper.lock.lock();
}3.3 解鎖和移除 Entry
解鎖時將等待的隊列減一。
當(dāng)前 key 對應(yīng)的線程數(shù)為 0 時,可以將其從 ConcurrentHashMap 中移除。
public void unlock(String key) {
LockWrapper lockWrapper = locks.get(key);
lockWrapper.lock.unlock();
if (lockWrapper.removeThreadFromQueue() == 0) {
// NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
locks.remove(key, lockWrapper);
}
}3.4 總結(jié)
最終效果如下:
public class LockByKey {
private static class LockWrapper {
private final Lock lock = new ReentrantLock();
private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
private LockWrapper addThreadInQueue() {
numberOfThreadsInQueue.incrementAndGet();
return this;
}
private int removeThreadFromQueue() {
return numberOfThreadsInQueue.decrementAndGet();
}
}
private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
public void lock(String key) {
LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
lockWrapper.lock.lock();
}
public void unlock(String key) {
LockWrapper lockWrapper = locks.get(key);
lockWrapper.lock.unlock();
if (lockWrapper.removeThreadFromQueue() == 0) {
// NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
locks.remove(key, lockWrapper);
}
}
}
使用示例:
String key = "key";
LockByKey lockByKey = new LockByKey();
try {
lockByKey.lock(key);
// insert your code here
} finally { // CRUCIAL
lockByKey.unlock(key);
}四、允許同一個 key 同時多個線程運(yùn)行
我們還需要考慮另外一種場景: 前面對于同一個 key 同一時刻只允許一個線程執(zhí)行。如果我們想實(shí)現(xiàn),對于同一個 key ,允許同時運(yùn)行 n 個線程該怎么辦?
為了方便理解,我們假設(shè)同一個 key 允許兩個線程。
- 第一個線程想要獲取 某個 key 的鎖,允許
- 第二個線程也想要獲取該 key 的鎖,允許
- 第三個線程也想獲取該 key 的鎖,該線程需要等待第一個或第二個線程釋放鎖之后才可以執(zhí)行
Semaphore 很適合這種場景。Semaphore 可以控制同時運(yùn)行的線程數(shù)。
public class SimultaneousEntriesLockByKey {
private static final int ALLOWED_THREADS = 2;
private static ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
public void lock(String key) {
Semaphore semaphore = semaphores.compute(key, (k, v) -> v == null ? new Semaphore(ALLOWED_THREADS) : v);
semaphore.acquireUninterruptibly();
}
public void unlock(String key) {
Semaphore semaphore = semaphores.get(key);
semaphore.release();
if (semaphore.availablePermits() == ALLOWED_THREADS) {
semaphores.remove(key, semaphore);
}
}
}使用案例:
String key = "key";
SimultaneousEntriesLockByKey lockByKey = new SimultaneousEntriesLockByKey();
try {
lockByKey.lock(key);
// 在這里添加對該 key 獲取鎖之后要執(zhí)行的代碼
} finally { // 非常關(guān)鍵
lockByKey.unlock(key);
}五、結(jié)論
本文演示如何對某個 key 加鎖,以保證對該 key 的并發(fā)操作限制,可以實(shí)現(xiàn)同一個 key 一個或者多個線程同時執(zhí)行。
相關(guān)代碼:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-4
原文:https://www.baeldung.com/java-acquire-lock-by-key
到此這篇關(guān)于Java 中通過 key 獲取鎖的文章就介紹到這了,更多相關(guān)java 獲取鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android設(shè)備如何保證數(shù)據(jù)同步寫入磁盤的實(shí)現(xiàn)
這篇文章主要介紹了Android設(shè)備如何保證數(shù)據(jù)同步寫入磁盤的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Java基于Tcp協(xié)議的socket編程實(shí)例
這篇文章主要介紹了Java基于Tcp協(xié)議的socket編程實(shí)例,較為詳細(xì)的分析了socket編程客戶端與服務(wù)器端的具體實(shí)現(xiàn)步驟與使用技巧,具有一定的參考借鑒價值,需要的朋友可以參考下2014-12-12
Spring Boot/Angular整合Keycloak實(shí)現(xiàn)單點(diǎn)登錄功能
Keycloak新的發(fā)行版命名為Quarkus,專為GraalVM和OpenJDK HotSpot量身定制的一個Kurbernetes Native Java框架,計劃2019年底正式發(fā)布。這篇文章主要介紹了Spring Boot/Angular整合Keycloak實(shí)現(xiàn)單點(diǎn)登錄,需要的朋友可以參考下2019-10-10
springboot嵌套子類使用方式—前端與后臺開發(fā)的注意事項
這篇文章主要介紹了springboot嵌套子類使用方式—前端與后臺開發(fā)的注意事項,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
TransactionSynchronization的invokeAfterCompletion事務(wù)源碼解析
這篇文章主要為大家介紹了TransactionSynchronization的invokeAfterCompletion事務(wù)源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Intellij IDEA 2017新特性之Spring Boot相關(guān)特征介紹
Intellij IDEA 2017.2.2版本針對Springboot設(shè)置了一些特性,本篇文章給大家簡單介紹一下如何使用這些特性,需要的朋友參考下吧2018-01-01

