基于String實(shí)現(xiàn)同步鎖的方法步驟
在某些時(shí)候,我們可能想基于字符串做一些事情,比如:針對(duì)同一用戶的并發(fā)同步操作,使用鎖字符串的方式實(shí)現(xiàn)比較合理。因?yàn)橹挥性谙嗤址那闆r下,并發(fā)操作才是不被允許的。而如果我們不分青紅皂白直接全部加鎖,那么整體性能就下降得厲害了。
因?yàn)閟tring的多樣性,看起來string鎖是天然比分段鎖之類的高級(jí)鎖更有優(yōu)勢呢。
因?yàn)镾tring 類型的變量賦值是這樣的: String a = "hello world."; 所有往往會(huì)有個(gè)錯(cuò)誤的映象,String對(duì)象就是不可變的。
額,關(guān)于這個(gè)問題的爭論咱們就不細(xì)說了,總之, "a" != "a" 是有可能成立的。
另外,針對(duì)上鎖這件事,我們都知道,鎖是要針對(duì)同一個(gè)對(duì)象,才會(huì)有意義。所以,粗略的,我們可以這樣使用字符串鎖:
public void method1() {
String str1 = "a";
synchronized (str1) {
// do sync a things...
}
}
public void method2() {
String str2 = "a";
synchronized (str2) {
// do sync b things...
}
}
乍一看,這的確很方便簡單。但是,前面說了, "a" 是可能不等于 "a" 的(這是大部分情況,只有當(dāng)String被存儲(chǔ)在常量池中時(shí)值相同的String變量才相等)。
所以,我們可以稍微優(yōu)化下:
public void method3() {
String str1 = "a";
synchronized (str1.intern()) {
// do sync a things...
}
}
public void method4() {
String str2 = "a";
synchronized (str2.intern()) {
// do sync b things...
}
}
看起來還是很方便簡單的,其原理就是把String對(duì)象放到常量池中。但是會(huì)有個(gè)問題,這些常量池的數(shù)據(jù)如何清理呢?
不管怎么樣,我們是不是可以自己去基于String實(shí)現(xiàn)一個(gè)鎖呢?
肯定是可以的了!直接上代碼!
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
/**
* 基于string 的鎖實(shí)現(xiàn)
*/
public final class StringBasedMutexLock {
private static final Logger logger = LoggerFactory.getLogger(StringBasedMutexLock.class);
/**
* 字符鎖 管理器, 將每個(gè)字符串 轉(zhuǎn)換為一個(gè) CountDownLatch
*
* 即鎖只會(huì)發(fā)生在真正有并發(fā)更新 同一個(gè) String 的情況下
*
*/
private static final ConcurrentMap<String, CountDownLatch> lockKeyHolder = new ConcurrentHashMap<>();
/**
* 基于lockKey 上鎖,同步執(zhí)行
*
* @param lockKey 字符鎖
*/
public static void lock(String lockKey) {
while (!tryLock(lockKey)) {
try {
logger.debug("【字符鎖】并發(fā)更新鎖升級(jí), {}", lockKey);
blockOnSecondLevelLock(lockKey);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("【字符鎖】中斷異常:" + lockKey, e);
break;
}
}
}
/**
* 釋放 lockKey 對(duì)應(yīng)的鎖選項(xiàng),使其他線程可執(zhí)行
*
* @param lockKey 要使用互斥的字符串
* @return true: 釋放成功, false: 釋放失敗,可能被其他線程誤釋放
*/
public static boolean unlock(String lockKey) {
// 先刪除鎖,再釋放鎖,此處會(huì)導(dǎo)致后續(xù)進(jìn)來的并發(fā)優(yōu)先執(zhí)行,無影響
CountDownLatch realLock = getAndReleaseLock1(lockKey);
releaseSecondLevelLock(realLock);
return true;
}
/**
* 嘗試給指定字符串上鎖
*
* @param lockKey 要使用互斥的字符串
* @return true: 上鎖成功, false: 上鎖失敗
*/
private static boolean tryLock(String lockKey) {
// 此處會(huì)導(dǎo)致大量 ReentrantLock 對(duì)象創(chuàng)建嗎?
// 其實(shí)不會(huì)的,這個(gè)數(shù)量最大等于外部并發(fā)數(shù),只是對(duì) gc 不太友好,會(huì)反復(fù)創(chuàng)建反復(fù)銷毀y
return lockKeyHolder.putIfAbsent(lockKey, new CountDownLatch(1)) == null;
}
/**
* 釋放1級(jí)鎖(刪除) 并返回重量級(jí)鎖
*
* @param lockKey 字符鎖
* @return 真正的鎖
*/
private static CountDownLatch getAndReleaseLock1(String lockKey) {
return lockKeyHolder.remove(lockKey);
}
/**
* 二級(jí)鎖鎖定(鎖升級(jí))
*
* @param lockKey 鎖字符串
* @throws InterruptedException 中斷時(shí)拋出異常
*/
private static void blockOnSecondLevelLock(String lockKey) throws InterruptedException {
CountDownLatch realLock = getRealLockByKey(lockKey);
// 為 null 說明此時(shí)鎖已被刪除, next race
if(realLock != null) {
realLock.await();
}
}
/**
* 二級(jí)鎖解鎖(如有必要)
*
* @param realLock 鎖實(shí)例
*/
private static void releaseSecondLevelLock(CountDownLatch realLock) {
realLock.countDown();
}
/**
* 通過key 獲取對(duì)應(yīng)的鎖實(shí)例
*
* @param lockKey 字符串鎖
* @return 鎖實(shí)例
*/
private static CountDownLatch getRealLockByKey(String lockKey) {
return lockKeyHolder.get(lockKey);
}
}
使用時(shí),只需傳入 lockKey 即可。
// 加鎖 StringBasedMutexLock.lock(linkKey); // 解鎖 StringBasedMutexLock.unlock(linkKey);
這樣做有什么好處嗎?
1. 使用ConcurrentHashMap實(shí)現(xiàn)鎖獲取,性能還是不錯(cuò)的;
2. 每個(gè)字符串對(duì)應(yīng)一個(gè)鎖,使用完成后就刪除,不會(huì)導(dǎo)致內(nèi)存溢出問題;
3. 可以作為一個(gè)外部工具使用,業(yè)務(wù)代碼接入方便,無需像 synchronized 一樣,需要整段代碼包裹起來;
不足之處?
1. 使用ConcurrentHashMap實(shí)現(xiàn)鎖獲取,性能還是不錯(cuò)的;
2. 每個(gè)字符串對(duì)應(yīng)一個(gè)鎖,使用完成后就刪除,不會(huì)導(dǎo)致內(nèi)存溢出問題;
3. 可以作為一個(gè)外部工具使用,業(yè)務(wù)代碼接入方便,無需像 synchronized 一樣,需要整段代碼包裹起來;
4. 本文只是想展示實(shí)現(xiàn) String 鎖,此鎖并不適用于分布式場景下的并發(fā)處理;
擴(kuò)展: 如果不使用 String 做鎖,如何保證大并發(fā)前提下的小概率并發(fā)場景的線程安全?
我們知道 CAS 的效率是比較高的,我們可以使用原子類來進(jìn)行CAS的操作。
比如,我們添加一狀態(tài)字段, 操作此字段以保證線程安全:
/**
* 運(yùn)行狀態(tài)
*
* 4: 正在刪除, 1: 正在放入隊(duì)列中, 0: 正常無運(yùn)行
*/
private transient volatile AtomicInteger runningStatus = new AtomicInteger(0);
// 更新時(shí)先獲取該狀態(tài):
public void method5() {
AtomicInteger runningStatus = link.getRunningStatus();
// 正在刪除數(shù)據(jù)過程中,則等待
if(!runningStatus.compareAndSet(0, 1)) {
// 1. 等待另外線程刪除完成
// 2. 刪除正在更新標(biāo)識(shí)
// 3. 重新運(yùn)行本次數(shù)據(jù)放入邏輯
long lockStartTime = System.currentTimeMillis();
long maxLockTime = 10 * 1000;
while (!runningStatus.compareAndSet(0, 1)) {
if(System.currentTimeMillis() - lockStartTime > maxLockTime) {
break;
}
}
runningStatus.compareAndSet(1, 0);
throw new RuntimeException("數(shù)據(jù)正在更新,重新運(yùn)行: " + link.getLinkKey() + link);
}
try {
// do sync things
}
finally {
runningStatus.compareAndSet(1, 0);
}
}
public void method6() {
AtomicInteger runningStatus = link.getRunningStatus();
if (!runningStatus.compareAndSet(0, 4)) {
logger.error(" 數(shù)據(jù)正在更新中,不得刪除,返回 ");
return;
}
try {
// do sync things
}
catch (Exception e) {
logger.error("并發(fā)更新異常:", e);
}
finally {
runningStatus.compareAndSet(4, 0);
}
}
實(shí)際測試下來,CAS 性能是要比 synchronized 之類的鎖性能要好的。當(dāng)然,我們這里針對(duì)的并發(fā)數(shù)都是極少的,我們只是想要保證這極少情況下的線程安全性。所以,其實(shí)也還好。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
java使用EasyExcel導(dǎo)入導(dǎo)出excel
導(dǎo)入導(dǎo)出excel數(shù)據(jù)是常見的需求,今天就來看一下Java基于EasyExcel實(shí)現(xiàn)這個(gè)功能,感興趣的朋友可以了解下2021-05-05
Java Web項(xiàng)目中Spring框架處理JSON格式數(shù)據(jù)的方法
Spring MVC是個(gè)靈活的框架,返回JSON數(shù)據(jù)的也有很多五花八門的方式,這里我們來整理一個(gè)最簡單的Java Web項(xiàng)目中Spring框架處理JSON格式數(shù)據(jù)的方法:2016-05-05
在實(shí)踐中了解Java反射機(jī)制應(yīng)用
當(dāng)程序運(yùn)行時(shí),允許改變程序結(jié)構(gòu)或變量類型,這種語言稱為動(dòng)態(tài)語言。我們認(rèn)為java并不是動(dòng)態(tài)語言,但是它卻有一個(gè)非常突出的動(dòng)態(tài)相關(guān)機(jī)制,俗稱:反射。下面我們來簡單學(xué)習(xí)一下吧2019-05-05
springboot HandlerIntercepter攔截器修改request body數(shù)據(jù)的操作
這篇文章主要介紹了springboot HandlerIntercepter攔截器修改request body數(shù)據(jù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2021-06-06
Spring中的后置處理器BeanPostProcessor詳解
這篇文章主要介紹了Spring中的后置處理器BeanPostProcessor詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
深入探究一下Java中不同的線程間數(shù)據(jù)通信方式
這篇文章主要來和大家一起深入探究一下Java中不同的線程間數(shù)據(jù)通信方式,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下2023-04-04

