在之前的文章中,我們介紹了讀寫鎖,學習完之后你應該已經知道了讀寫鎖允許多個線程同時訪問共享變量,適用于讀多寫少的場景。那么在讀多寫少的場景中還有沒有更快的技術方案呢?還真有,在Java1.8這個版本里提供了一種叫StampedLock的鎖,它的性能就比讀寫鎖還要好。
下面我們介就來介紹一下StampedLock的使用方法、內部工作原理以及在使用過程中需要注意的事項。
StampedLock支持的三種模式
我們先來看看StampedLock在使用什么,和上篇文章中的ReadWriteLock所有哪些區別。
ReadWriteLock支持兩種模式,一種是讀鎖,一種是寫鎖,而StampedLock支持三種模式,分別是寫鎖、悲觀鎖鎖、樂觀讀,其中寫鎖、悲觀讀鎖的語義和ReadWriteLock的寫鎖、讀鎖的語義非常類似。允許多個線程同時獲取悲觀讀鎖,但是只允許一個線程獲取寫鎖,寫鎖和悲觀讀鎖都是互斥的,然而不同的是里面的寫鎖和悲觀讀鎖加鎖成功之后,都會返回一個stamp。然后解鎖的時候需要傳入這個stamp。相關的實例代碼如下。
final StampedLock sl = new StampedLock();//獲取/釋放悲觀讀鎖示意代碼
long stamp = sl.reaLock();
try{//省略業務代碼
} finally {sl.unlockRead(stamp);
}//獲取/釋放寫鎖示意代碼
long stamp = sl.writeLock();
try{//省略相關業務代碼
} finally {sl.unlockWrite(stamp);
}
StampedLock 的性能之所以比ReadWriteLock還要好,關鍵是StampedLock支持樂觀讀的方式。ReadWriteLock支持多個線程同時讀,但是當多個線程同時讀的時候,所有的寫操作也會被阻塞。而StampedLock提供的樂觀讀是允許一個線程獲取寫鎖的,也就是說不是所有寫操作都被阻塞的。
注意,這里我們用的是"樂觀讀"這個詞,而不是樂觀讀鎖,是要提醒你,樂觀讀操作是無鎖的,所以相比較ReadWriteLock的讀鎖,樂觀讀的性能更好一點。文中下面這段代碼是出自于Java SDK官方示例,并略作修改。在distanceFromRrigin()這個方法中,首先通過調用tryOptimisticRead獲得了一個stamp。這里的tryOptimisticRead就是我們前面提到的樂觀讀。之后將共享變量X和Y讀入方法的局部變量中。不過需要注意的是,由于tryOptimisticRead是無鎖的,所以共享變量X和Y讀入方法局部變量時,X和Y有可能被其他線程修改了,因此最后讀完之后還需要再次驗證一下是否存在寫操作,這個操作是通過調用validate (stamp)來實現的。
class Point {private int x,y;final StampedLock sl = new StampedLock();//計算到原點的距離int distanceFromOrigin(){//樂觀讀long stamp = sl.tryOptimisticRead();//讀入局部變量//讀的過程中數據可能被修改int curX = x,curY = y;//判斷執行讀操作期間,是否存在寫操作//如果存在,返回falseif(!sl.validate(stamp)){//升級為悲觀鎖stamp = sl.readLock();try{curX = x;curY = y;} finally {sl.unLockRead(stamp);}}return Math.sqrt(curX * curX + curY * curY);}
}
在上面這個代碼示例中,如果執行樂觀讀操作期間存在寫操作,會把樂觀讀升級為悲觀讀鎖。這個做法挺合理的,否則你就需要在一個循環里反復執行樂觀讀,直到執行樂觀讀操作期間沒有寫操作,只有這樣才能保證X和Y的正確性和一致性。而循環讀會浪費大量的CPU。升級為悲觀讀鎖代碼簡練且不易出錯,建議你在具體實踐的時候也采用這樣的方法。
進一步理解樂觀讀
如果你曾經用過數據庫的樂觀鎖,你可能會發現StampLock的樂觀讀和數據庫的樂觀讀鎖有異曲同工之妙。的確是這樣的,就我個人而言,我是先接觸數據庫的樂觀鎖,然后再接觸的StampLock,我就覺得我前期數據庫里的樂觀鎖的學習,對于后面的理解StampLock的樂觀讀有很大的幫助,所以這里有必要再介紹一下數據庫里的樂觀鎖。
還記得我第一次使用數據庫樂觀鎖的場景是這樣的,在ERP的生產模塊里,會有多個人通過ERP系統提供的UI同時修改同一條生產訂單,那如何保證生產訂單數據是并發安全的呢?我采用的方案就是樂觀鎖。
樂觀鎖的實現很簡單,在生產訂單的表product_doc里面增加一個數字型的版本號字段version,每次更新product_doc這個表的時候,都將version字段加1。生產訂單的UI在展示的時候需要查詢數據庫。此時將這個version字段和其他業務字段一起返回給生產訂單UI。假設用戶查詢的生產訂單的ID=777,那么SQL語句類似于下面這樣。
select id,....,version
from product_doc
where id = 777
用戶在生產訂單UI執行保存操作時候,后臺利用下面的SQL語句更新生產訂單,此時我們假設該條生產訂單的version等于9。
update product_doc
set version=version+1
where id=777 and versoin=9
如果這條語SQL語句成執行成功,并且返回的條數等于1,那么說明從生產訂單UI執行查詢操作到執行保存操作期間沒有其他人修改過這條數據。因為如果這期間其他人修改過這條數據,那么版本號一定會大于9。
你會發現數據庫里的樂觀鎖查詢的時候需要把version字段查出來,更新的時候要利用version字段做校驗,這個version字段就類似于StampLock里面的stamp,這樣對比著看,你相信你會更容易理解StampLock里面樂觀讀的用法。
StampLock使用注意事項
對于讀多寫少的場景StampLock性能很好,簡單的應用場景基本上可以替代ReadWriteLock,但是StampLock的功能僅僅是ReadWriteLock的子集,在使用的時候還是有幾個需地方需要注意一下。
StampLock在命名上并沒有增加Reentrant,想必你已經猜到了,StampLock應該是不可重入的,事實上的確是這樣的,StampLock不支持重入,這個是在使用中必須要注意的。
另外StampLock的悲觀讀鎖、寫鎖都不支持條件變量,這個你也需要注意。
還有一點需要特別注意,那就是如果線程阻塞在StampLock的readLock()上時,此時調用該阻塞線程的interrupt()方法會導致CPU飆升。
所以使用StampLock一定不要調用中斷操作,如果需要支持中斷功能,一定使用可中斷的悲觀讀鎖readLockInterruptibly()和寫鎖writeLockInterruptibly(),這個規則一定要記清楚。
總結
StampLock的使用看上去有點復雜,但是如果你能理解樂觀所背后的原理,使用起來還是比較流暢的。