Java中的StampedLock實(shí)現(xiàn)原理詳解
StampedLock(郵戳鎖/版本鎖/票據(jù)鎖)
jdk8引入 讀讀不互斥,讀寫不互斥,寫寫互斥
- ReentrantReadWriteLock采用悲觀讀,第一個(gè)讀線程拿到鎖后,第二個(gè)/第三個(gè)讀線程可以拿到鎖,特別是在讀線程很多,寫線程很少時(shí),寫線程可能一直拿不到鎖,在非公平鎖的情況下,導(dǎo)致寫線程餓死
- StampedLock引入樂觀讀,讀時(shí)不加讀鎖,寫鎖也可進(jìn)行寫,避免寫線程被餓死,讀數(shù)據(jù)時(shí)讀出來發(fā)現(xiàn)數(shù)據(jù)被修改了,再升級為悲觀讀,再讀一次
- 所有獲取鎖的方法,都返回一個(gè)郵戳(Stamp),為0表示獲取失敗,其余值都表示獲取成功
- 所有釋放鎖的方法,都需要一個(gè)郵戳(Stamp),必須和成功獲取鎖時(shí)的一致
stamp是long類型,代表鎖的狀態(tài),當(dāng)歸0時(shí),表示線程獲取鎖失敗,當(dāng)釋放鎖或轉(zhuǎn)換鎖時(shí),都要傳入最初的stamp值
不可重入,如果一個(gè)線程已經(jīng)獲取寫鎖,再去獲取寫鎖的話容易死鎖
三種模式
- read(讀悲觀模式): 功能和ReentrantReadWriteLock的讀鎖類似,在讀時(shí)不允許有寫的操作
- write(悲觀寫模式): 功能和ReentrantReadWriteLock的寫鎖功能類似
- Optimistic(樂觀讀模式): 樂觀鎖,讀時(shí)可有寫操作,讀完后檢驗(yàn)版本號是否被寫改了,若改了就再悲觀讀一次
代碼演示
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 多個(gè)線程調(diào)用該方法,修改x和y的值
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 獲取寫鎖
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 釋放寫鎖
}
}
// 多個(gè)線程調(diào)用該方法,求距離
double distenceFromOrigin() {
// 樂觀讀
long stamp = sl.tryOptimisticRead();
// 將共享變量拷貝到線程棧, 讀:將一份數(shù)據(jù)拷貝到線程的棧內(nèi)存中
double currentX = x, currentY = y;
// 讀期間有其他線程修改數(shù)據(jù), 讀取后,對比讀之前的版本號和當(dāng)前的版本號,判斷數(shù)據(jù)是否可用
// 根據(jù)stamp判斷在讀取數(shù)據(jù)和使用數(shù)據(jù)期間,有沒有其他線程修改數(shù)據(jù)
if (!sl.validate(stamp)) {
// 讀到的是臟數(shù)據(jù),丟棄.重新使用悲觀讀
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
如上代碼,有一個(gè)Point類,多個(gè)線程調(diào)用move()方法,修改坐標(biāo);還有多個(gè)線程調(diào)用distanceFromOrigin()方法,求距離 首先,執(zhí)行move操作時(shí),要加寫鎖,和ReadWriteLock的用法沒有區(qū)別,寫操作和寫操作也是互斥的 在讀時(shí),用樂觀讀sl.tryOptimisticRead(),相當(dāng)于在讀之前給數(shù)據(jù)的狀態(tài)做一個(gè)快照,把數(shù)據(jù)拷貝到內(nèi)存里面,在用之前,再比對一次版本號,如果版本號變了,則說明在讀的期間有其他線程修改了數(shù)據(jù),讀出來的數(shù)據(jù)廢棄,重新悲觀讀獲取數(shù)據(jù)
要說明的是,這三行關(guān)鍵代碼對順序非常敏感,不能有重排序。因?yàn)閟tate變量已經(jīng)是volatile,所以可以禁止重排序,但stamp并不是volatile的。為此,在validate(stamp)方法里面插入內(nèi)存屏障
public boolean validate(long stamp) {
VarHandle.acquireFence();
return (stamp & SBITS) == (state & SBITS);
}
public class StampedLockDemo {
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public void write() {
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName()+"\t"+"寫線程準(zhǔn)備修改");
try {
number = number + 13;
}finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName()+"\t"+"寫線程結(jié)束修改");
}
//悲觀讀,讀沒有完成時(shí)候?qū)戞i無法獲得鎖
public void read() {
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName()+"\t"+" come in readlock code block,4 seconds continue...");
for (int i = 0; i < 4; i++) {
// 暫停幾秒鐘線程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+" 正在讀取中......");
}
try {
int result = number;
System.out.println(Thread.currentThread().getName()+"\t"+" 獲得成員變量值result:"+result);
System.out.println("寫線程沒有修改成功,讀鎖時(shí)候?qū)戞i無法介入,傳統(tǒng)的讀寫互斥");
}finally {
stampedLock.unlockRead(stamp);
}
}
// 樂觀讀,讀的過程中也允許獲取寫鎖介入
public void tryOptimisticRead() {
long stamp = stampedLock.tryOptimisticRead();
int result = number;
// 故意間隔4秒鐘,很樂觀認(rèn)為讀取中沒有其它線程修改過number值,具體靠判斷
System.out.println("4秒前stampedLock.validate方法值(true無修改,false有修改)"+"\t"+stampedLock.validate(stamp));
for (int i = 0; i < 4; i++) {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"正在讀取... "+i+" 秒" +
"后stampedLock.validate方法值(true無修改,false有修改)"+"\t"+stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)) {
System.out.println("有人修改過------有寫操作");
stamp = stampedLock.readLock();
try {
System.out.println("從樂觀讀 升級為 悲觀讀");
result = number;
System.out.println("重新悲觀讀后result:"+result);
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);
}
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
/* 傳統(tǒng)版
new Thread(() -> {
resource.read();
},"readThread").start();
//暫停幾秒鐘線程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
resource.write();
},"writeThread").start();
//暫停幾秒鐘線程
try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"number:" +number);*/
new Thread(() -> {
resource.tryOptimisticRead();
},"readThread").start();
// 暫停2秒鐘線程,讀過程可以寫介入,演示
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
// 暫停6秒鐘線程
// try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
resource.write();
},"writeThread").start();
}
}樂觀讀實(shí)現(xiàn)原理
StampedLock是一個(gè)讀寫鎖,因此也會像讀寫鎖那樣,把一個(gè)state變量分成兩半,分別表示讀鎖和寫鎖的狀態(tài)。
同時(shí),還需要一個(gè)數(shù)據(jù)的version,但是,一次CAS沒有辦法操作兩個(gè)變量,所以這個(gè)state變量本身同時(shí)也表示了數(shù)據(jù)的version。
下面先分析state變量
public class StampedLock implements java.io.Serializable {
private static final int LG_READERS = 7;
private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS; // 第8位表示寫鎖
private static final long RBITS = WBIT - 1L; // 最低的7位表示讀鎖
private static final long RFULL = RBITS - 1L; // 讀鎖的數(shù)目
private static final long ABITS = RBITS | WBIT; // 讀鎖和寫鎖狀態(tài)合二為一
private static final long SBITS = ~RBITS;
private static final long ORIGIN = WBIT << 1; // state的初始值
private transient volatile long state;
}
用最低的8位表示讀和寫的狀態(tài),其中第8位表示寫鎖的狀態(tài),最低的7位表示讀鎖的狀態(tài)。
因?yàn)閷戞i只有一個(gè)bit位,所以寫鎖是不可重入的
初始值不為0,而是把WBIT 向左移動了一位,也就是上面的ORIGIN 常量,構(gòu)造方法如下所示
為什么state的初始值不設(shè)為0呢?
看樂觀鎖的實(shí)現(xiàn):
上面兩個(gè)方法必須結(jié)合起來看:當(dāng)state&WBIT != 0的時(shí)候,說明有線程持有寫鎖,上面的tryOptimisticRead會永遠(yuǎn)返回0。
這樣,再調(diào)用validate(stamp),也就是validate(0)也會永遠(yuǎn)返回false。這正是我們想要的邏輯:當(dāng)有線程持有寫鎖的時(shí)候,validate永遠(yuǎn)返回false,無論寫線程是否釋放了寫鎖。
因?yàn)闊o論是否釋放了(state回到初始值)寫鎖,state值都不為0,所以validate(0)永遠(yuǎn)為false
為什么上面的validate(…)方法不直接比較stamp=state,而要比較state&SBITS=state&SBITS 呢?
因?yàn)樽x鎖和讀鎖是不互斥的! 所以,即使在“樂觀讀”的時(shí)候,state 值被修改了,但如果它改的是第7位,validate(…)還是會返回true。
另外要說明的一點(diǎn)是,上面使用了內(nèi)存屏障VarHandle.acquireFence();,是因?yàn)樵谶@行代碼的下一行里面的stamp、SBITS變量不是volatile的,由此可以禁止其和前面的currentX=X,currentY=Y進(jìn)行重排序 通過上面的分析,可以發(fā)現(xiàn)state的設(shè)計(jì)非常巧妙。
只通過一個(gè)變量,既實(shí)現(xiàn)了讀鎖、寫鎖的狀態(tài)記錄,還實(shí)現(xiàn)了數(shù)據(jù)的版本號的記錄
到此這篇關(guān)于Java中的StampedLock實(shí)現(xiàn)原理詳解的文章就介紹到這了,更多相關(guān)StampedLock實(shí)現(xiàn)原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于@PostConstruct、afterPropertiesSet和init-method的執(zhí)行順序
這篇文章主要介紹了關(guān)于@PostConstruct、afterPropertiesSet和init-method的執(zhí)行順序,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
詳解SpringSecurity如何實(shí)現(xiàn)前后端分離
這篇文章主要為大家介紹了詳解SpringSecurity如何實(shí)現(xiàn)前后端分離,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
SpringBatch數(shù)據(jù)處理之ItemProcessor鏈與異常處理技巧
Spring Batch的ItemProcessor體系為批處理應(yīng)用提供了強(qiáng)大而靈活的數(shù)據(jù)處理能力,通過深入理解Spring Batch的ItemProcessor設(shè)計(jì)理念和應(yīng)用技巧,開發(fā)者可以充分發(fā)揮其潛力,滿足各類企業(yè)級批處理需求,感興趣的朋友一起看看吧2025-03-03
在java中判斷兩個(gè)浮點(diǎn)型(float)數(shù)據(jù)是否相等的案例
這篇文章主要介紹了在java中判斷兩個(gè)浮點(diǎn)型(float)數(shù)據(jù)是否相等的案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
解決Spring JPA 使用@transaction注解時(shí)產(chǎn)生CGLIB代理沖突問題
這篇文章主要介紹了解決Spring JPA 使用@transaction注解時(shí)產(chǎn)生CGLIB代理沖突問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
spring boot使用sharding jdbc的配置方式
這篇文章主要介紹了spring boot使用sharding jdbc的配置方式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
Java中關(guān)于Collections集合工具類的詳細(xì)介紹
Java提供了一個(gè)操作Set、List和Map等集合的工具類:Collections,該工具提供了大量方法對集合元素進(jìn)行排序、查詢和修改等操作,還提供了將集合對象設(shè)置為不可變、對集合對象實(shí)現(xiàn)同步控制等方法2021-09-09
Java?SpringBoot集成文件之如何使用POI導(dǎo)出Word文檔
這篇文章主要介紹了Java?SpringBoot集成文件之如何使用POI導(dǎo)出Word文檔,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08

