2023年一起來認識一下StampedLock吧

大雄45發表於2023-02-05
導讀 本文主要講解了StampedLock的功能和使用,至於原理,StampedLock雖然不像其它鎖一樣定義了內部類來實現AQS框架,但是StampedLock的基本實現思路還是利用CLH佇列進行執行緒的管理,透過同步狀態值來表示鎖的狀態和型別,具體的原始碼實現大家感興趣的自己可以追蹤看看。
概述

想到讀寫鎖,大家第一時間想到的可能是ReentrantReadWriteLock。實際上,在jdk8以後,java提供了一個效能更優越的讀寫鎖併發類StampedLock,該類的設計初衷是作為一個內部工具類,用於輔助開發其它執行緒安全元件,用得好,該類可以提升系統效能,用不好,容易產生死鎖和其它莫名其妙的問題。本文主要和大家一起學習下StampedLock的功能和使用。

StampedLock介紹

StampedLock的狀態由版本和模式組成。鎖獲取方法返回一個戳,該戳表示並控制對鎖狀態的訪問。StampedLock提供了3種模式控制訪問鎖:

寫模式

獲取寫鎖,它是獨佔的,當鎖處於寫模式時,無法獲得讀鎖,所有樂觀讀驗證都將失敗。

  • writeLock(): 阻塞等待獨佔獲取鎖,返回一個戳, 如果是0表示獲取失敗。
  • tryWriteLock():嘗試獲取一個寫鎖,返回一個戳, 如果是0表示獲取失敗。
  • long tryWriteLock(long time, TimeUnit unit): 嘗試獲取一個獨佔寫鎖,可以等待一段事件,返回一個戳, 如果是0表示獲取失敗。
  • long writeLockInterruptibly(): 試獲取一個獨佔寫鎖,可以被中斷,返回一個戳, 如果是0表示獲取失敗。
  • unlockWrite(long stamp):釋放獨佔寫鎖,傳入之前獲取的戳。
  • tryUnlockWrite():如果持有寫鎖,則釋放該鎖,而不需要戳值。這種方法可能對錯誤後的恢復很有用。
long stamp = lock.writeLock();
try {
    ....
} finally {
    lock.unlockWrite(stamp);
}
讀模式

悲觀的方式後去非獨佔讀鎖。

  • readLock(): 阻塞等待獲取非獨佔的讀鎖,返回一個戳, 如果是0表示獲取失敗。
  • tryReadLock():嘗試獲取一個讀鎖,返回一個戳, 如果是0表示獲取失敗。
  • long tryReadLock(long time, TimeUnit unit): 嘗試獲取一個讀鎖,可以等待一段事件,返回一個戳, 如果是0表示獲取失敗。
  • long readLockInterruptibly(): 阻塞等待獲取非獨佔的讀鎖,可以被中斷,返回一個戳, 如果是0表示獲取失敗。
  • unlockRead(long stamp):釋放非獨佔的讀鎖,傳入之前獲取的戳。
  • tryUnlockRead():如果讀鎖被持有,則釋放一次持有,而不需要戳值。這種方法可能對錯誤後的恢復很有用。
  • long stamp = lock.readLock();
    try {
        ....
    } finally {
        lock.unlockRead(stamp);        
    }
    樂觀讀模式

    樂觀讀也就是若讀的操作很多,寫的操作很少的情況下,你可以樂觀地認為,寫入與讀取同時發生機率很少,因此不悲觀地使用完全的讀取鎖定,程式可以檢視讀取資料之後,是否遭到寫入執行的變更,再採取後續的措施(重新讀取變更資訊,或者丟擲異常) ,這一個小小改進,可大幅度提高程式的吞吐量。

    StampedLock支援 tryOptimisticRead()方法,讀取完畢後做一次戳校驗,如果校驗透過,表示這期間沒有其他執行緒的寫操作,資料可以安全使用,如果校驗沒透過,需要重新獲取讀鎖,保證資料一致性。

  • tryOptimisticRead(): 返回稍後可以驗證的戳記,如果獨佔鎖定則返回零。
  • boolean validate(long stamp): 如果自給定戳記發行以來鎖還沒有被獨佔獲取,則返回true。
  • long stamp = lock.tryOptimisticRead();
    // 驗戳
    if(!lock.validate(stamp)){
      // 鎖升級
    }

    此外,StampedLock 提供了api實現上面3種方式進行轉換:

  • long tryConvertToWriteLock(long stamp)
  • 如果鎖狀態與給定的戳記匹配,則執行以下操作之一。如果戳記表示持有寫鎖,則返回它。或者,如果是讀鎖,如果寫鎖可用,則釋放讀鎖並返回寫戳記。或者,如果是樂觀讀,則僅在立即可用時返回寫戳記。該方法在所有其他情況下返回零

  • long tryConvertToReadLock(long stamp)
  • 如果鎖狀態與給定的戳記匹配,則執行以下操作之一。如果戳記表示持有寫鎖,則釋放它並獲得讀鎖。或者,如果是讀鎖,返回它。或者,如果是樂觀讀,則僅在立即可用時才獲得讀鎖並返回讀戳記。該方法在所有其他情況下返回零。

  • long tryConvertToOptimisticRead(long stamp)
  • 如果鎖狀態與給定的戳記匹配,那麼如果戳記表示持有鎖,則釋放它並返回一個觀察戳記。或者,如果是樂觀讀,則在驗證後返回它。該方法在所有其他情況下返回0,因此作為“tryUnlock”的形式可能很有用。

    演示例子

    下面用一個例子演示下StampedLock的使用,例子來源jdk中的javadoc。

    @Slf4j
    @Data
    public class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();
        void move(double deltaX, double throws{
            //涉及對共享資源的修改,使用寫鎖-獨佔操作
            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");
            }
        }
        /**
         * 使用樂觀讀鎖訪問共享資源
         * 注意:樂觀讀鎖在保證資料一致性上需要複製一份要操作的變數到方法棧,並且在運算元據時候可能其他寫執行緒已經修改了資料,
         * 而我們操作的是方法棧裡面的資料,也就是一個快照,所以最多返回的不是最新的資料,但是一致性還是得到保障的。
         *
         * @return
         */
        double distanceFromOrigin() throws{
            long stamp = sl.tryOptimisticRead();    // 使用樂觀讀鎖
            log.info("tryOptimisticRead lock success");
            // 睡一秒中
            Thread.sleep(1000);
            double currentX = x, currentY = y;      // 複製共享資源到本地方法棧中
            if (!sl.validate(stamp)) {              // 如果有寫鎖被佔用,可能造成資料不一致,所以要切換到普通讀鎖模式
                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{ // 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);
            }
        }
    }

    測試用例:

    @Test
    public void testStamped() throws InterruptedException {
        Point point = new Point();
        point.setX(1);
        point.setY(2);
        // 執行緒0 執行了樂觀讀
        Thread thread0 = new Thread(() -> {
            try {
                // 樂觀讀
                point.distanceFromOrigin();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread-0");
        thread0.start();
        Thread.sleep(500);
        // 執行緒1 執行寫鎖
        Thread thread1 = new Thread(() -> {
            // 樂觀讀
            try {
                point.move(3, 4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread-1");
        thread1.start();
        thread0.join();
        thread1.join();
    }

    結果:

    2023年一起來認識一下StampedLock吧2023年一起來認識一下StampedLock吧

    效能對比
    正是由於StampedLock的樂觀讀模式,早就StampedLock的高效能和高吞吐量,那麼具體的效能提高有多少呢?

    下圖是和ReadWritLock相比,在一個執行緒情況下,讀速度是其4倍左右,寫是1倍。

    2023年一起來認識一下StampedLock吧2023年一起來認識一下StampedLock吧

    下圖是16個執行緒情況下,讀效能是其幾十倍,寫效能也是近10倍左右:

    2023年一起來認識一下StampedLock吧2023年一起來認識一下StampedLock吧

    下圖是吞吐量提高:

    2023年一起來認識一下StampedLock吧2023年一起來認識一下StampedLock吧

    那麼這樣是不是說StampedLock可以全方位的替代ReentrantReadWriteLock, 答案是否定的,StampedLock相對於ReentrantReadWriteLock有下面兩個問題:

  • 不支援條件變數Condition
  • 不支援可重入
  • 所以最終選擇StampedLock還是ReentrantReadWriteLock,還是要看具體的業務場景。

    總結

    本文主要講解了StampedLock的功能和使用,至於原理,StampedLock雖然不像其它鎖一樣定義了內部類來實現AQS框架,但是StampedLock的基本實現思路還是利用CLH佇列進行執行緒的管理,透過同步狀態值來表示鎖的狀態和型別,具體的原始碼實現大家感興趣的自己可以追蹤看看。

    原文來自:


    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2933660/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章