StampedLock:一個并發編程中非常重要的票據鎖

摘要:一起來聊聊這個在高并發環境下比ReadWriteLock更快的鎖——StampedLock 。
本文分享自華為云社區《【高并發】一文徹底理解并發編程中非常重要的票據鎖——StampedLock》,作者: 冰 河。
什么是StampedLock?ReadWriteLock鎖允許多個線程同時讀取共享變量,但是在讀取共享變量的時候,不允許另外的線程多共享變量進行寫操作,更多的適合于讀多寫少的環境中 。那么,在讀多寫少的環境中 , 有沒有一種比ReadWriteLock更快的鎖呢?
答案當然是有!那就是我們今天要介紹的主角——JDK1.8中新增的StampedLock!沒錯,就是它!
StampedLock與ReadWriteLock相比,在讀的過程中也允許后面的一個線程獲取寫鎖對共享變量進行寫操作,為了避免讀取的數據不一致,使用StampedLock讀取共享變量時,需要對共享變量進行是否有寫入的檢驗操作,并且這種讀是一種樂觀讀 。
總之,StampedLock是一種在讀取共享變量的過程中,允許后面的一個線程獲取寫鎖對共享變量進行寫操作,使用樂觀讀避免數據不一致的問題,并且在讀多寫少的高并發環境下,比ReadWriteLock更快的一種鎖 。
StampedLock三種鎖模式這里,我們可以簡單對比下StampedLock與ReadWriteLock,ReadWriteLock支持兩種鎖模式:一種是讀鎖,另一種是寫鎖,并且ReadWriteLock允許多個線程同時讀共享變量,在讀時,不允許寫 , 在寫時 , 不允許讀 , 讀和寫是互斥的,所以,ReadWriteLock中的讀鎖,更多的是指悲觀讀鎖 。
StampedLock支持三種鎖模式:寫鎖、讀鎖(這里的讀鎖指的是悲觀讀鎖)和樂觀讀(很多資料和書籍寫的是樂觀讀鎖,這里我個人覺得更準確的是樂觀讀,為啥呢?我們繼續往下看?。?。其中,寫鎖和讀鎖與ReadWriteLock中的語義類似,允許多個線程同時獲取讀鎖,但是只允許一個線程獲取寫鎖,寫鎖和讀鎖也是互斥的 。
另一個與ReadWriteLock不同的地方在于:StampedLock在獲取讀鎖或者寫鎖成功后,都會返回一個Long類型的變量,之后在釋放鎖時,需要傳入這個Long類型的變量 。例如,下面的偽代碼所示的邏輯演示了StampedLock如何獲取鎖和釋放鎖 。
public class StampedLockDemo{ //創建StampedLock鎖對象 public StampedLock stampedLock = new StampedLock(); //獲取、釋放讀鎖 public void testGetAndReleaseReadLock(){ long stamp = stampedLock.readLock(); try{ //執行獲取讀鎖后的業務邏輯 }finally{ //釋放鎖 stampedLock.unlockRead(stamp); } } //獲取、釋放寫鎖 public void testGetAndReleaseWriteLock(){ long stamp = stampedLock.writeLock(); try{ //執行獲取寫鎖后的業務邏輯 。}finally{ //釋放鎖 stampedLock.unlockWrite(stamp); } }}StampedLock支持樂觀讀,這是它比ReadWriteLock性能要好的關鍵所在 。 ReadWriteLock在讀取共享變量時,所有對共享變量的寫操作都會被阻塞 。而StampedLock提供的樂觀讀,在多個線程讀取共享變量時,允許一個線程對共享變量進行寫操作 。
我們再來看一下JDK官方給出的StampedLock示例 , 如下所示 。
class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) { // an exclusively locked method long stamp = sl.writeLock(); try {x += deltaX;y += deltaY; } finally { sl.unlockWrite(stamp); } } double distanceFromOrigin() { // A read-only method long stamp = sl.tryOptimisticRead(); double currentX = x, currentY = y; if (!sl.validate(stamp)) {stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); } } 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); if (ws != 0L) {stamp = ws;x = newX;y = newY; break; } else { sl.unlockRead(stamp);stamp = sl.writeLock(); } } } finally { sl.unlock(stamp); } }}在上述代碼中 , 如果在執行樂觀讀操作時,另外的線程對共享變量進行了寫操作,則會把樂觀讀升級為悲觀讀鎖 , 如下代碼片段所示 。
double distanceFromOrigin() { // A read-only method //樂觀讀 long stamp = sl.tryOptimisticRead(); double currentX = x, currentY = y; //判斷是否有線程對變量進行了寫操作 //如果有線程對共享變量進行了寫操作 //則sl.validate(stamp)會返回false if (!sl.validate(stamp)) { //將樂觀讀升級為悲觀讀鎖stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { //釋放悲觀鎖 sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY);}這種將樂觀讀升級為悲觀讀鎖的方式相比一直使用樂觀讀的方式更加合理 , 如果不升級為悲觀讀鎖,則程序會在一個循環中反復執行樂觀讀操作,直到樂觀讀操作期間沒有線程執行寫操作 , 而在循環中不斷的執行樂觀讀會消耗大量的CPU資源,升級為悲觀讀鎖是更加合理的一種方式 。

推薦閱讀