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