前言
ReadWriteLock
适用于讀多寫少的場景,允許多個線程同時讀取共享變量。但在讀多寫少的場景中,還有更快的技術方案。在Java 1.8中, 提供了
StampedLock
鎖,它的性能就比讀寫鎖還要好。下面我們介紹StampedLock的使用方法、内部工作原理以及在使用過程中需要注意的事項。
StampedLock支援的三種鎖模式
ReadWriteLock
支援兩種通路模式:讀鎖和寫鎖,而
StampedLock
支援三種通路模式:寫鎖、悲觀讀鎖和樂觀讀。
其中寫鎖和悲觀讀鎖的語義與ReadWriteLock中的寫鎖和讀鎖語義類似,允許多個線程同時擷取悲觀讀鎖,隻允許一個線程擷取寫鎖。與ReadWriteLock不同的是,StampedLock中的寫鎖和悲觀讀鎖加鎖成功之後,都會傳回一個stamp标記,然後解鎖的時候需要傳入這個stamp。
相關示例代碼如下(代碼來自參考[1])
final StampedLock sl = new StampedLock();
// 擷取/釋放悲觀讀鎖示意代碼
long stamp = sl.readLock();
try {
//省略業務相關代碼
} finally {
sl.unlockRead(stamp);
}
// 擷取/釋放寫鎖示意代碼
long stamp = sl.writeLock();
try {
//省略業務相關代碼
} finally {
sl.unlockWrite(stamp);
}
StampedLock的性能之是以比ReadWriteLock好,其關鍵在于StampedLock支援樂觀讀。ReadWriteLock支援多個線程同時讀,當多個線程同時讀的時候,所有的寫操作都會被阻塞。但是,StampedLock提供了樂觀讀,當有多個線程同時讀共享變量允許一個線程擷取寫鎖,也就是說不是所有寫操作都會被阻塞。
需要注意,StampedLock提供的是“樂觀讀”而不是“樂觀讀鎖”,這表示樂觀讀是無鎖的,這也是其比ReadWriteLock讀鎖性能好的原因。
樂觀讀的使用示例(代碼來自參考[1]):
class Point{
private int x, y;
final StampedLock sl = new StampedLock();
// 計算到原點的距離
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); //樂觀讀
//讀取全局變量存儲到局部變量中 在讀入的過程中,資料可能被修改
int curX = x;
int curY = y;
//判斷進行讀操作期間,是否存在寫操作,如果存在,則sl.validate(stamp)傳回false
if(!sl.validate(stamp)) {
stamp = sl.readLock(); //更新為悲觀讀鎖 一切的寫操作都會被阻塞
try {
curX = x;
curY = y;
}finally {
sl.unlockRead(stamp); //釋放悲觀讀鎖
}
}
return Math.sqrt(curX*curX + curY*curY);
}
}
我們将共享變量x,y讀入方法的局部變量中,因為
tryOptimisticRead()
是無鎖的,是以,共享變量x和y讀入方法局部變量時,x和y有可能被其他線程修改了。是以,最後讀完之後,還需要再次驗證一下在讀入過程中是否存在寫操作,這個驗證操作是通過調用
validate(stamp)
來實作的。
如果在執行樂觀讀操作期間,存在寫操作,會把樂觀讀更新為悲觀讀鎖。
如果不使用這種做法,那麼就可能需要使用循環來執行反複讀,直到執行樂觀讀操作的期間沒有寫操作,但是循環會浪費大量的CPU。
是以,更新為悲觀讀鎖,代碼簡練且不易出錯。
StampedLock樂觀讀的了解
資料庫中的樂觀鎖與StampedLock中的樂觀讀有着異曲同工之妙。
通過下面這個例子來了解:
在ERP的生産子產品中,會有多個人通過ERP系統提供的UI同時修改同一條生産訂單,那如何保證生産訂單資料是并發安全的?
一種解決方案是采用樂觀鎖。
在生産訂單的表
product_doc
裡面增加了一個資料型版本号字段
vresion
,每次更新product_doc這個表的時候,都将version字段加1。生産訂單的UI在展示的時候,需要查詢資料庫,此時将這個version字段和其他業務字段一起傳回給生産訂單UI。
假設使用者查詢的生産訂單的id=777,那麼SQL語句類似如下:
select id, ..., version
from product_doc
where id=777
使用者在生産訂單UI執行儲存操作的時候,背景利用下面的SQL語句更新生産訂單,此處我們假設該條生産訂單的version=4:
update product_doc
set version=version+1,...
where id=777 and version=4
如果這條SQL語句執行成功并且傳回條數等于1,那麼說明從生産訂單UI執行查詢操作到執行儲存期間,沒有其他人修改過這條資料。因為如果這期間有人修改過這條資料,那麼版本号字段一定會大于4。
資料庫中的樂觀鎖,查詢的時候,需要把version字段查出來,更新的時候要利用version字段做驗證。StampedLock裡面的stamp就類似于這個version字段。
StampedLock使用注意事項
StampedLock的功能僅僅是ReadWriteLock的子集,是以在使用時,還是需要注意一些地方:
- StampedLock在命名上沒有增加
,是以,猜想StampedLock不支援重入。事實上,确實如此,StampedLock是不支援重入的。Reentrant
- StampedLock的悲觀讀鎖、寫鎖都不支援條件變量。
- 如果線程阻塞在 StampedLock 的
或者readLock()
上時,調用該阻塞線程的writeLock()
方法,會導緻 CPU 飙升。(代碼來自參考[1])interrupt()
final StampedLock lock = new StampedLock(); Thread T1 = new Thread(()->{ lock.writeLock(); // 擷取寫鎖 LockSupport.park(); // 永遠阻塞在此處,不釋放寫鎖 }); T1.start(); Thread.sleep(100); // 保證T1擷取寫鎖 Thread T2 = new Thread(()->lock.readLock() ); //阻塞在悲觀讀鎖 T2.start(); Thread.sleep(100); // 保證T2阻塞在讀鎖 //中斷線程T2 會導緻線程T2所在CPU飙升 T2.interrupt(); T2.join();
線程 T1 擷取寫鎖之後将自己阻塞,線程 T2 嘗試擷取悲觀讀鎖,也會阻塞;如果此時調用線程 T2 的 interrupt() 方法來中斷線程 T2 的話,會發現線程 T2 所在 CPU 會飙升到 100%。(看專欄時明白線程T2擷取悲觀讀鎖會被阻塞,但是直到現在也不明白為什麼調用T2的interrupt()方法會導緻CPU飙升,望路過的看官解答。)
替代方法便是使用悲觀讀鎖
和寫鎖readLockInterruptibly()
。writeLockInterruptibly()
StampedLock官方示例使用讀寫鎖模闆
精簡Java官方示例後,可形成如下模闆(代碼來自參考[1])
StampedLock讀模闆:
final StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 樂觀讀
// 讀入方法局部變量
//......
// 校驗stamp
if (!sl.validate(stamp)){
stamp = sl.readLock(); // 更新為悲觀讀鎖
try {
// 讀入方法局部變量
.....
} finally {
sl.unlockRead(stamp); //釋放悲觀讀鎖
}
}
//使用方法局部變量執行業務操作
//......
StampedLock寫模闆:
long stamp = sl.writeLock();
try {
// 寫共享變量
......
} finally {
sl.unlockWrite(stamp);
}
小結
這篇部落格是學習專欄時的筆記總結出來的結果,粗略地介紹了一下
StampedLock
,欲知更詳細的請參考[3],無意中發現的大神部落格,推薦起(•̀ᴗ•́)و ̑̑
參考:
[1] 極客時間專欄王寶令《Java并發程式設計實戰》
[2] whoshiyeguiren.資料庫樂觀鎖和悲觀鎖的了解和實作(轉載&總結).https://blog.csdn.net/woshiyeguiren/article/details/80277475
[3] Ressmix.Java多線程進階(十一)—— J.U.C之locks架構:StampedLock.https://segmentfault.com/a/1190000015808032?utm_source=tag-newest
每天進步一點點,不要停止前進的腳步~