Java并發(fā)之搞懂讀寫(xiě)鎖
ReentrantReadWriteLock
我們來(lái)探討一下java.concurrent.util包下的另一個(gè)鎖,叫做ReentrantReadWriteLock,也叫讀寫(xiě)鎖。
實(shí)際項(xiàng)目中常常有這樣一種場(chǎng)景:

比如有一個(gè)共享資源叫做Some Data,多個(gè)線程去操作Some Data,這個(gè)操作有讀操作也有寫(xiě)操作,并且是讀多寫(xiě)少的,那么在沒(méi)有寫(xiě)操作的時(shí)候,多個(gè)線程去讀Some Data是不會(huì)有線程安全問(wèn)題的,因?yàn)榫€程只是訪問(wèn),并沒(méi)有修改,不存在競(jìng)爭(zhēng),所以這種情況應(yīng)該允許多個(gè)線程同時(shí)讀取Some Data。
但是若某個(gè)瞬間,線程X正在修改Some Data的時(shí)候,那么就不允許其他線程對(duì)Some Data做任何操作,否則就會(huì)有線程安全問(wèn)題。
那么針對(duì)這種讀多寫(xiě)少的場(chǎng)景,J.U.C包提供了ReentrantReadWriteLock,它包含了兩個(gè)鎖:
- ReadLock:讀鎖,也被稱(chēng)為共享鎖
- WriteLock:寫(xiě)鎖,也被稱(chēng)為排它鎖
下面我們看看,線程如果想獲取讀鎖,需要具備哪些條件:
- 不能有其他線程的寫(xiě)鎖沒(méi)有寫(xiě)請(qǐng)求;
- 或者有寫(xiě)請(qǐng)求,但調(diào)用線程和持有鎖的線程是同一個(gè)
再來(lái)看一下線程獲取寫(xiě)鎖的條件:
- 必須沒(méi)有其他線程的讀鎖
- 必須沒(méi)有其他線程的寫(xiě)鎖
這個(gè)比較容易理解,因?yàn)閷?xiě)鎖是排他的。
來(lái)看下面一段代碼:
public class ReentrantReadWriteLockTest {
private Object data;
//緩存是否有效
private volatile boolean cacheValid;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void processCachedData() {
rwl.readLock().lock();
//如果緩存無(wú)效,更新cache;否則直接使用data
if (!cacheValid) {
//獲取寫(xiě)鎖前必須釋放讀鎖
rwl.readLock().unlock();
rwl.writeLock().lock();
if (!cacheValid) {
//更新數(shù)據(jù)
data = new Object();
cacheValid = true;
}
//鎖降級(jí),在釋放寫(xiě)鎖前獲取讀鎖
rwl.readLock().lock();
//釋放寫(xiě)鎖,依然持有讀鎖
rwl.writeLock().unlock();
}
// 使用緩存
// ...
// 釋放讀鎖
rwl.readLock().unlock();
}
}
這段代碼演示的是獲取緩存的時(shí)候,判斷緩存是否過(guò)期,如果已經(jīng)過(guò)期就更新緩存,如果沒(méi)有過(guò)期就使用緩存。
可以看到我們先創(chuàng)建了一個(gè)讀鎖,判斷如果緩存有效,就可以使用緩存,使用完之后再把讀鎖釋放。如果緩存無(wú)效,就更新緩存執(zhí)行寫(xiě)操作,所以先把讀鎖給釋放掉,然后創(chuàng)建一個(gè)寫(xiě)鎖,最后更新緩存,更新完緩存后又重新獲取了一個(gè)讀鎖并釋放掉寫(xiě)鎖。
從這段代碼里可以看出來(lái),一個(gè)線程在拿到寫(xiě)鎖之后它還可以繼續(xù)獲得一個(gè)讀鎖。
小結(jié)
我們來(lái)總結(jié)一下ReentrantReadWriteLock的三個(gè)特性:
- 公平性
ReentrantReadWriteLock也可以在初始化時(shí)設(shè)置是否公平。
- 可重入性
讀鎖以及寫(xiě)鎖也是支持重入的,比如一個(gè)線程拿到寫(xiě)鎖后,他依然可以繼續(xù)拿寫(xiě)鎖,同理讀鎖也可以。
- 鎖降級(jí)
要想實(shí)現(xiàn)鎖降級(jí),只需要先獲得寫(xiě)鎖,再獲得讀鎖,最后釋放寫(xiě)鎖,就可以把一個(gè)寫(xiě)鎖降級(jí)為讀鎖了。但是一個(gè)讀鎖是沒(méi)有辦法升級(jí)為寫(xiě)鎖的。
最后我們來(lái)對(duì)比一下ReentrantLock與ReentrantReadWriteLock
ReentrantLock:完全互斥ReentrantReadWriteLock:讀鎖共享,寫(xiě)鎖互斥
因此在讀多寫(xiě)少的場(chǎng)景下,ReentrantReadWriteLock的性能、吞吐量各方面都會(huì)比ReentrantLock要好很多。但是對(duì)于寫(xiě)多的場(chǎng)景ReentrantReadWriteLock就不那么明顯了。
StampedLock
上面我們已經(jīng)探討了ReentrantReadWriteLock能夠大幅度提升讀多寫(xiě)少場(chǎng)景下的性能,StampedLock是在JDK8引入的,可以認(rèn)為這是一個(gè)ReentrantReadWriteLock的增強(qiáng)版。
那么大家想,既然有了ReentrantReadWriteLock,為什么還要搞一個(gè)StampedLock呢?
這是因?yàn)镽eentrantReadWriteLock在一些特定的場(chǎng)景下存在問(wèn)題。
比如寫(xiě)線程的“饑餓”問(wèn)題。
舉個(gè)例子:假設(shè)現(xiàn)在有超級(jí)多的線程在操作ReentrantReadWriteLock,執(zhí)行讀操作的線程超級(jí)多,而執(zhí)行寫(xiě)操作的線程很少,而如果這個(gè)執(zhí)行寫(xiě)操作的線程想要拿到寫(xiě)鎖,而ReentrantReadWriteLock的寫(xiě)鎖是排他的,要想拿到寫(xiě)鎖就意味著其他線程不能有讀鎖也不能有寫(xiě)鎖,所以在讀線程超級(jí)多,寫(xiě)線程超級(jí)少的情況下就容易造成寫(xiě)線程饑餓問(wèn)題,也就是說(shuō),執(zhí)行寫(xiě)操作的線程可能一直搶不到鎖,即使可以把公平性設(shè)置為true,但是這樣又會(huì)導(dǎo)致性能的下降。
那么我們看看StampedLock怎么玩:
首先,所有獲取鎖的方法都會(huì)返回stamp,它是一個(gè)數(shù)字,如果stamp=0說(shuō)明操作失敗了,其他的值表示操作成功。
其次就是所有獲取鎖的方法,需要用stamp作為參數(shù),參數(shù)的值必須和獲得鎖時(shí)返回的stamp一致。
其中StampedLock提供了三種訪問(wèn)模式:
Writing模式:類(lèi)似于ReentrantReadWriteLock的寫(xiě)鎖Reding(悲觀讀模式):類(lèi)似于ReentrantReadWriteLock的讀鎖。Optimistic reading:樂(lè)觀讀模式
悲觀讀模式:在執(zhí)行悲觀讀的過(guò)程中,不允許有寫(xiě)操作
樂(lè)觀讀模式:在執(zhí)行樂(lè)觀讀的過(guò)程中,允許有寫(xiě)操作
通過(guò)介紹我們可以發(fā)現(xiàn),StampedLock中的悲觀讀與樂(lè)觀讀和我們操作數(shù)據(jù)庫(kù)中的悲觀鎖、樂(lè)觀鎖有一定的相似之處。
此外StampedLock還提供了讀鎖和寫(xiě)鎖相互轉(zhuǎn)換的功能:
我們知道ReentrantReadWriteLock的寫(xiě)鎖是可以降級(jí)為讀鎖的,但是讀鎖沒(méi)辦法升級(jí)為寫(xiě)鎖,而StampedLock它提供了讀鎖和寫(xiě)鎖之間互相轉(zhuǎn)換的功能。
最后,StampedLock是不可重入的,這也是和ReentrantReadWriteLock的一個(gè)區(qū)別。
讀過(guò)源碼的同學(xué)可能知道,在StampedLock源碼里有一段注釋?zhuān)?br />

我們來(lái)看一下這段注釋?zhuān)麑?xiě)的非常經(jīng)典,演示了StampedLock API如何使用。
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
//添加寫(xiě)鎖
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
//釋放寫(xiě)鎖
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
//獲得一個(gè)樂(lè)觀鎖
long stamp = sl.tryOptimisticRead();
// 假設(shè)(x,y)=(10,10)
// 但是這是一個(gè)樂(lè)觀讀鎖,(x,y)可能被其他線程修改為(20,20)
double currentX = x, currentY = y;
//因此這里要驗(yàn)證獲得樂(lè)觀鎖后,有沒(méi)有發(fā)生寫(xiě)操作
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX currentX + currentY currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}

這個(gè)類(lèi)有三個(gè)方法,move方法用來(lái)移動(dòng)一個(gè)點(diǎn)的坐標(biāo),instanceFromOrigin用來(lái)計(jì)算這個(gè)點(diǎn)到原點(diǎn)的距離,moveIfAtOrigin表示當(dāng)這個(gè)點(diǎn)位于原點(diǎn)的時(shí)候用來(lái)移動(dòng)這個(gè)點(diǎn)的坐標(biāo)。
我們來(lái)分析一下源碼:
move方法是一個(gè)純粹的寫(xiě)操作,在操作之前添加寫(xiě)鎖,操作結(jié)束釋放寫(xiě)鎖;
instanceOrigin首先獲得一個(gè)樂(lè)觀鎖,然后開(kāi)始讀數(shù)據(jù),我們假設(shè)(x,y)=(10,10),但是這是一個(gè)樂(lè)觀讀鎖,(x,y)可能被其他線程修改為(20,20),所以他會(huì)驗(yàn)證獲得樂(lè)觀鎖后,有沒(méi)有發(fā)生寫(xiě)操作,如果validate結(jié)果為true的話,表示沒(méi)有發(fā)生過(guò)寫(xiě)操作,如果發(fā)生過(guò)寫(xiě)操作,那么就會(huì)改用悲觀讀鎖重讀數(shù)據(jù),然后計(jì)算結(jié)果,當(dāng)然最后要把鎖釋放掉。
最后moveIfAtOrigin方法也比較簡(jiǎn)單,主要演示了怎么從悲觀讀鎖轉(zhuǎn)換成寫(xiě)鎖。
小結(jié)
StampedLock主要通過(guò)樂(lè)觀讀的方式提升性能,同時(shí)也解決了寫(xiě)線程的饑餓問(wèn)題,但是有得必有失,我們從示例代碼中不難看出,StampedLock使用起來(lái)要比ReentrantReadWriteLock復(fù)雜很多,所以使用者要在性能和復(fù)雜度之間做一個(gè)取舍。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
- java并發(fā)編程StampedLock高性能讀寫(xiě)鎖詳解
- java并發(fā)編程中ReentrantLock可重入讀寫(xiě)鎖
- Java并發(fā)編程之ReadWriteLock讀寫(xiě)鎖的操作方法
- Java并發(fā)編程之重入鎖與讀寫(xiě)鎖
- Java并發(fā)編程之顯示鎖ReentrantLock和ReadWriteLock讀寫(xiě)鎖
- Java多線程之ReentrantReadWriteLock源碼解析
- Java多線程 ReentrantReadWriteLock原理及實(shí)例詳解
- 一文了解Java讀寫(xiě)鎖ReentrantReadWriteLock的使用
- 詳解Java?ReentrantReadWriteLock讀寫(xiě)鎖的原理與實(shí)現(xiàn)
- Java并發(fā)讀寫(xiě)鎖ReentrantReadWriteLock 使用場(chǎng)景
相關(guān)文章
如何利用Spring把元素解析成BeanDefinition對(duì)象
這篇文章主要介紹了如何利用Spring把元素解析成BeanDefinition對(duì)象,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
logstash將mysql數(shù)據(jù)同步到elasticsearch方法詳解
這篇文章主要為大家介紹了logstash將mysql數(shù)據(jù)同步到elasticsearch方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
基于java中的流程控制語(yǔ)句總結(jié)(必看篇)
下面小編就為大家?guī)?lái)一篇基于java中的流程控制語(yǔ)句總結(jié)(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
springboot整合shiro登錄失敗次數(shù)限制功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了springboot整合shiro-登錄失敗次數(shù)限制功能,實(shí)現(xiàn)此功能如果是防止壞人多次嘗試,破解密碼的情況,所以要限制用戶(hù)登錄嘗試次數(shù),需要的朋友可以參考下2018-09-09
Java開(kāi)發(fā)環(huán)境配置JDK超詳細(xì)整理(適合新手入門(mén))
這篇文章主要給大家介紹了關(guān)于Java開(kāi)發(fā)環(huán)境配置JDK超詳細(xì)整理的相關(guān)資料,非常適合新手入門(mén),JDK是Java語(yǔ)言的軟件開(kāi)發(fā)工具包,主要用于移動(dòng)設(shè)備、嵌入式設(shè)備上的java應(yīng)用程序,需要的朋友可以參考下2023-11-11
如何使用?Spring?Boot?搭建?WebSocket?服務(wù)器實(shí)現(xiàn)多客戶(hù)端連接
本文介紹如何使用SpringBoot快速搭建WebSocket服務(wù)器,實(shí)現(xiàn)多客戶(hù)端連接和消息廣播,WebSocket協(xié)議提供全雙工通信,SpringBoot通過(guò)@ServerEndpoint簡(jiǎn)化配置,支持實(shí)時(shí)消息推送,適用于聊天室或通知系統(tǒng)等應(yīng)用場(chǎng)景2024-11-11

