Java利用StampedLock實(shí)現(xiàn)讀寫(xiě)鎖的方法詳解
概述
想到讀寫(xiě)鎖,大家第一時(shí)間想到的可能是ReentrantReadWriteLock
。實(shí)際上,在jdk8以后,java提供了一個(gè)性能更優(yōu)越的讀寫(xiě)鎖并發(fā)類StampedLock
,該類的設(shè)計(jì)初衷是作為一個(gè)內(nèi)部工具類,用于輔助開(kāi)發(fā)其它線程安全組件,用得好,該類可以提升系統(tǒng)性能,用不好,容易產(chǎn)生死鎖和其它莫名其妙的問(wèn)題。本文主要和大家一起學(xué)習(xí)下StampedLock
的功能和使用。
StampedLock介紹
StampedLock
的狀態(tài)由版本和模式組成。鎖獲取方法返回一個(gè)戳,該戳表示并控制對(duì)鎖狀態(tài)的訪問(wèn)。StampedLock
提供了3種模式控制訪問(wèn)鎖:
1.寫(xiě)模式
獲取寫(xiě)鎖,它是獨(dú)占的,當(dāng)鎖處于寫(xiě)模式時(shí),無(wú)法獲得讀鎖,所有樂(lè)觀讀驗(yàn)證都將失敗。
- writeLock(): 阻塞等待獨(dú)占獲取鎖,返回一個(gè)戳, 如果是0表示獲取失敗。
- tryWriteLock():嘗試獲取一個(gè)寫(xiě)鎖,返回一個(gè)戳, 如果是0表示獲取失敗。
- long tryWriteLock(long time, TimeUnit unit): 嘗試獲取一個(gè)獨(dú)占寫(xiě)鎖,可以等待一段事件,返回一個(gè)戳, 如果是0表示獲取失敗。
- long writeLockInterruptibly(): 試獲取一個(gè)獨(dú)占寫(xiě)鎖,可以被中斷,返回一個(gè)戳, 如果是0表示獲取失敗。
- unlockWrite(long stamp):釋放獨(dú)占寫(xiě)鎖,傳入之前獲取的戳。
- tryUnlockWrite():如果持有寫(xiě)鎖,則釋放該鎖,而不需要戳值。這種方法可能對(duì)錯(cuò)誤后的恢復(fù)很有用。
long stamp = lock.writeLock(); try { .... } finally { lock.unlockWrite(stamp); }
2.讀模式
悲觀的方式后去非獨(dú)占讀鎖。
- readLock(): 阻塞等待獲取非獨(dú)占的讀鎖,返回一個(gè)戳, 如果是0表示獲取失敗。
- tryReadLock():嘗試獲取一個(gè)讀鎖,返回一個(gè)戳, 如果是0表示獲取失敗。
- long tryReadLock(long time, TimeUnit unit): 嘗試獲取一個(gè)讀鎖,可以等待一段事件,返回一個(gè)戳, 如果是0表示獲取失敗。
- long readLockInterruptibly(): 阻塞等待獲取非獨(dú)占的讀鎖,可以被中斷,返回一個(gè)戳, 如果是0表示獲取失敗。
- unlockRead(long stamp):釋放非獨(dú)占的讀鎖,傳入之前獲取的戳。
- tryUnlockRead():如果讀鎖被持有,則釋放一次持有,而不需要戳值。這種方法可能對(duì)錯(cuò)誤后的恢復(fù)很有用。
long stamp = lock.readLock(); try { .... } finally { lock.unlockRead(stamp); }
3.樂(lè)觀讀模式
樂(lè)觀讀也就是若讀的操作很多,寫(xiě)的操作很少的情況下,你可以樂(lè)觀地認(rèn)為,寫(xiě)入與讀取同時(shí)發(fā)生幾率很少,因此不悲觀地使用完全的讀取鎖定,程序可以查看讀取資料之后,是否遭到寫(xiě)入執(zhí)行的變更,再采取后續(xù)的措施(重新讀取變更信息,或者拋出異常) ,這一個(gè)小小改進(jìn),可大幅度提高程序的吞吐量。
StampedLock
支持 tryOptimisticRead()
方法,讀取完畢后做一次戳校驗(yàn),如果校驗(yàn)通過(guò),表示這期間沒(méi)有其他線程的寫(xiě)操作,數(shù)據(jù)可以安全使用,如果校驗(yàn)沒(méi)通過(guò),需要重新獲取讀鎖,保證數(shù)據(jù)一致性。
- tryOptimisticRead(): 返回稍后可以驗(yàn)證的戳記,如果獨(dú)占鎖定則返回零。
- boolean validate(long stamp): 如果自給定戳記發(fā)行以來(lái)鎖還沒(méi)有被獨(dú)占獲取,則返回true。
long stamp = lock.tryOptimisticRead(); // 驗(yàn)戳 if(!lock.validate(stamp)){ // 鎖升級(jí) }
此外,StampedLock 提供了api實(shí)現(xiàn)上面3種方式進(jìn)行轉(zhuǎn)換:
long tryConvertToWriteLock(long stamp)
如果鎖狀態(tài)與給定的戳記匹配,則執(zhí)行以下操作之一。如果戳記表示持有寫(xiě)鎖,則返回它?;蛘撸绻亲x鎖,如果寫(xiě)鎖可用,則釋放讀鎖并返回寫(xiě)戳記?;蛘?,如果是樂(lè)觀讀,則僅在立即可用時(shí)返回寫(xiě)戳記。該方法在所有其他情況下返回零
long tryConvertToReadLock(long stamp)
如果鎖狀態(tài)與給定的戳記匹配,則執(zhí)行以下操作之一。如果戳記表示持有寫(xiě)鎖,則釋放它并獲得讀鎖?;蛘撸绻亲x鎖,返回它。或者,如果是樂(lè)觀讀,則僅在立即可用時(shí)才獲得讀鎖并返回讀戳記。該方法在所有其他情況下返回零。
long tryConvertToOptimisticRead(long stamp)
如果鎖狀態(tài)與給定的戳記匹配,那么如果戳記表示持有鎖,則釋放它并返回一個(gè)觀察戳記?;蛘?,如果是樂(lè)觀讀,則在驗(yàn)證后返回它。該方法在所有其他情況下返回0,因此作為“tryUnlock”的形式可能很有用。
演示例子
下面用一個(gè)例子演示下StampedLock的使用,例子來(lái)源jdk中的javadoc。
@Slf4j @Data public class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) throws InterruptedException { //涉及對(duì)共享資源的修改,使用寫(xiě)鎖-獨(dú)占操作 long stamp = sl.writeLock(); log.info("writeLock lock success"); Thread.sleep(500); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); log.info("unlock write lock success"); } } /** * 使用樂(lè)觀讀鎖訪問(wèn)共享資源 * 注意:樂(lè)觀讀鎖在保證數(shù)據(jù)一致性上需要拷貝一份要操作的變量到方法棧,并且在操作數(shù)據(jù)時(shí)候可能其他寫(xiě)線程已經(jīng)修改了數(shù)據(jù), * 而我們操作的是方法棧里面的數(shù)據(jù),也就是一個(gè)快照,所以最多返回的不是最新的數(shù)據(jù),但是一致性還是得到保障的。 * * @return */ double distanceFromOrigin() throws InterruptedException { long stamp = sl.tryOptimisticRead(); // 使用樂(lè)觀讀鎖 log.info("tryOptimisticRead lock success"); // 睡一秒中 Thread.sleep(1000); double currentX = x, currentY = y; // 拷貝共享資源到本地方法棧中 if (!sl.validate(stamp)) { // 如果有寫(xiě)鎖被占用,可能造成數(shù)據(jù)不一致,所以要切換到普通讀鎖模式 log.info("validate stamp error"); stamp = sl.readLock(); log.info("readLock success"); try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); log.info("unlock read success"); } } 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); //讀鎖轉(zhuǎn)換為寫(xiě)鎖 if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { sl.unlockRead(stamp); stamp = sl.writeLock(); } } } finally { sl.unlock(stamp); } } }
測(cè)試用例:
@Test public void testStamped() throws InterruptedException { Point point = new Point(); point.setX(1); point.setY(2); // 線程0 執(zhí)行了樂(lè)觀讀 Thread thread0 = new Thread(() -> { try { // 樂(lè)觀讀 point.distanceFromOrigin(); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread-0"); thread0.start(); Thread.sleep(500); // 線程1 執(zhí)行寫(xiě)鎖 Thread thread1 = new Thread(() -> { // 樂(lè)觀讀 try { point.move(3, 4); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread-1"); thread1.start(); thread0.join(); thread1.join(); }
結(jié)果:
性能對(duì)比
正是由于StampedLock
的樂(lè)觀讀模式,早就StampedLock
的高性能和高吞吐量,那么具體的性能提高有多少呢?
下圖是和ReadWritLock相比,在一個(gè)線程情況下,讀速度是其4倍左右,寫(xiě)是1倍。
下圖是16個(gè)線程情況下,讀性能是其幾十倍,寫(xiě)性能也是近10倍左右:
下圖是吞吐量提高:
那么這樣是不是說(shuō)StampedLock
可以全方位的替代ReentrantReadWriteLock
, 答案是否定的,StampedLock
相對(duì)于ReentrantReadWriteLock
有下面兩個(gè)問(wèn)題:
- 不支持條件變量Condition
- 不支持可重入
所以最終選擇StampedLock
還是ReentrantReadWriteLock
,還是要看具體的業(yè)務(wù)場(chǎng)景。
總結(jié)
本文主要講解了StampedLock
的功能和使用,至于原理,StampedLock
雖然不像其它鎖一樣定義了內(nèi)部類來(lái)實(shí)現(xiàn)AQS框架,但是StampedLock的基本實(shí)現(xiàn)思路還是利用CLH隊(duì)列進(jìn)行線程的管理,通過(guò)同步狀態(tài)值來(lái)表示鎖的狀態(tài)和類型,具體的源碼實(shí)現(xiàn)大家感興趣的自己可以追蹤看看。
以上就是Java利用StampedLock實(shí)現(xiàn)讀寫(xiě)鎖的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Java StampedLock讀寫(xiě)鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?Boot中調(diào)用外部接口的3種方式步驟
這篇文章主要給大家介紹了關(guān)于Spring?Boot中調(diào)用外部接口的3種方式步驟,在Spring-Boot項(xiàng)目開(kāi)發(fā)中,存在著本模塊的代碼需要訪問(wèn)外面模塊接口,或外部url鏈接的需求,需要的朋友可以參考下2023-08-08springboot 啟動(dòng)如何修改application.properties的參數(shù)
這篇文章主要介紹了springboot 啟動(dòng)如何修改application.properties的參數(shù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08spring?Cloud微服務(wù)阿里開(kāi)源TTL身份信息的線程間復(fù)用
這篇文章主要為大家介紹了spring?Cloud微服務(wù)中使用阿里開(kāi)源TTL身份信息的線程間復(fù)用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01springBoot+dubbo+zookeeper實(shí)現(xiàn)分布式開(kāi)發(fā)應(yīng)用的項(xiàng)目實(shí)踐
本文主要介紹了springBoot+dubbo+zookeeper實(shí)現(xiàn)分布式開(kāi)發(fā)應(yīng)用的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03深入解析Java中的Classloader的運(yùn)行機(jī)制
這篇文章主要介紹了Java中的Classloader的運(yùn)行機(jī)制,包括從JVM方面講解類加載器的委托機(jī)制等,需要的朋友可以參考下2015-11-11詳解如何通過(guò)Java實(shí)現(xiàn)壓縮PDF文檔
PDF文檔是我們?nèi)粘^k公中使用最頻繁的文檔格式。但因?yàn)榇蠖鄶?shù)PDF文檔都包含很多頁(yè)面圖像或大量圖片,這就導(dǎo)致PDF文檔過(guò)大,處理起來(lái)較為麻煩。本文將介紹如何通過(guò)Java應(yīng)用程序壓縮PDF文檔,需要的可以了解一下2022-12-12