天天看點

【漫畫】讀寫鎖ReadWriteLock還是不夠快?再試試StampedLock!資料庫中的鎖StampedLock總結

本文來源于公衆号【胖滾豬學程式設計】 轉載請注明出處!

互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock

一文中,我們對比了互斥鎖ReentrantLock和讀寫鎖ReadWriteLock的差別,說明了讀寫鎖在讀多寫少的場景下具有明顯的性能優勢,但是人的欲望是無窮的,還是不能被滿足。。

【漫畫】讀寫鎖ReadWriteLock還是不夠快?再試試StampedLock!資料庫中的鎖StampedLock總結

資料庫中的鎖

由于大部分碼農接觸鎖都是從資料庫中的鎖開始的,是以這裡不妨先聊聊資料庫中的鎖。

我們以火車票售票的例子,假設如下場景,兩處火車票售票點同時讀取某一趟列車車票資料庫中的餘票數量,然後兩處售票點同時賣出一張車票,同時修改餘票為 X -1,寫回資料庫,這樣就造成了實際賣出兩張火車票而資料庫中的記錄卻隻減少了一張。

如果你閱讀了公衆号【胖滾豬學程式設計】的并發系列文章,包括:

如何解決原子性問題

ReentrantLock互斥鎖 讀寫鎖ReadWriteLock

,那麼你一定知道出現原因和解決方案,對了,可以使用鎖。

鎖可以分為兩大類,樂觀鎖和悲觀鎖:

  • 悲觀鎖:顧名思義,就是很悲觀,總是假設最壞的情況,每次去拿資料的時候都認為别人會修改, 是以每次在拿資料的時候都會上鎖,這樣别人想拿這個資料就會阻塞直到它拿到鎖。
  • 樂觀鎖:樂觀鎖,每次去拿資料的時候想法都是“沒事,肯定沒被改過”,于是就開心地擷取到資料,不放心嗎?那就在更新的時候判斷一下在此期間别人有沒有去更新過這個資料,可以使用版本号等機制。

一般情況下,資料庫都會有讀共享寫獨占的鎖并發的方案,也就是說讀讀并發是沒問題的,但在讀寫并發時,則有可能出現讀取不一緻情況,也就是常說的髒讀,是以在悲觀鎖的模式下,在有寫線程的時候,是不允許有任何其他的讀和寫線程的,也就是說寫是獨占的,這樣會導緻系統的吞吐明顯下降。我們所說的ReadWriteLock的寫鎖就屬于悲觀鎖。

如何避免這一情況,答案是使用樂觀鎖。每個線程都不會修改原始資料,而是從原始資料上拷貝上一份資料,同時記錄版本号,不同的線程更新自己的資料,在最終寫會時會判斷版本号是否變更,如果變更則意味有人已經更改過了,那麼目前線程需要做的就是自旋重試,如果重試指定的次數依然失敗,那麼就應該放棄更新,這種政策僅僅适合寫并發并不強烈的場景,如果寫競争嚴重,那麼多次自旋重試的開銷也是非常耗性能的,如果競争激烈,那麼寫鎖獨占的方式則更加适合。

那麼具體怎麼使用版本号機制呢?

很簡單,對資料庫表添加了一個version字段,設定為bigint類型。查詢的時候我們需要查出版本資訊,更新的時候,需要将版本資訊+1。

1.查詢資料資訊
select xxx,version from xxx where id= #{id}
2.根據資料資訊是判斷目前資料庫中的version是否還是剛才查出來的那個version
update xxx set xxx=xxx ,version = version+1 where id=#{id} and version= #{version};           

由于update指定了where條件,可根據傳回修改記錄條數來判斷目前更新是否生效,如果成功改動0條資料,說明version發生了變更,這時候可以根據自己業務邏輯來決定是否需要復原事務。

資料庫裡的樂觀鎖,查詢的時候需要把 version 字段查出來,更新的時候要利用 version 字段做驗證。這個 version 字段就類似于今天我們要說的 StampedLock 裡面的 stamp。基于上面談到的這些内容,我們再來分析StampedLock類,就會非常比較容易了解。

本文來源于公衆号【胖滾豬學程式設計】 以漫畫形式讓程式設計so easy and interesting !轉載請注明出處!

StampedLock

Java 在 1.8 這個版本裡,提供了一種叫 StampedLock 的鎖,它的性能比讀寫鎖還要好。

對比ReadWriteLock

我們先來看看StampedLock 和上一篇文章講的 ReadWriteLock 有哪些差別。

ReadWriteLock 支援兩種模式:一種是讀鎖,一種是寫鎖。而 StampedLock 支援三種模式,分别是:寫鎖、悲觀讀鎖和樂觀讀。

其中,寫鎖、悲觀讀鎖的語義和 ReadWriteLock 的寫鎖、讀鎖的語義非常類似,允許多個線程同時擷取悲觀讀鎖,但是隻允許一個線程擷取寫鎖,寫鎖和悲觀讀鎖是互斥的。

不同的是:StampedLock 裡的寫鎖和悲觀讀鎖加鎖成功之後,都會傳回一個 stamp;然後解鎖的時候,需要傳入這個 stamp,這裡的stamp就類似剛剛我們說的資料庫version,相信你已經明白了。

我們通過代碼示範一下寫鎖、悲觀讀鎖是如何使用的:

// 鎖執行個體
    private final StampedLock sl = new StampedLock();

    // 排它鎖-寫鎖
    void writeLock() {
        long stamp = sl.writeLock();//擷取寫鎖
        try {
          // 業務邏輯
        } finally {
            sl.unlockWrite(stamp);//釋放寫鎖
        }
    }

    // 悲觀讀鎖
    void readLock() {
        long stamp = sl.readLock();
        try {
          // 業務邏輯
        } finally {
            sl.unlockRead(stamp);
        }
    }
           

樂觀讀

StampedLock 的性能之是以比 ReadWriteLock 還要好,其關鍵是 StampedLock 支援樂觀讀的方式。所謂樂觀讀,即讀的時候也能允許一個線程擷取寫鎖,也就是說不是所有的寫操作都被阻塞,自然而然的會比所有寫都阻塞性能要強。

還是通過代碼來說明一下樂觀讀是如何使用的:

// 樂觀讀
    double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();//(1)
        double currentX = x, currentY = y;

        // 檢查在(1)擷取到讀鎖票據後,鎖有沒被其他寫線程排它性搶占
        if (!sl.validate(stamp)) {
            // 如果被搶占則擷取一個共享讀鎖(悲觀讀鎖)
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX*currentX + currentY*currentY);
    }           

tryOptimisticRead() 就是我們前面提到的樂觀讀。不過需要注意的是,由于 tryOptimisticRead() 是無鎖的,是以共享變量 x 和 y 讀入方法局部變量時,x 和 y 有可能被其他線程修改了。是以最後讀完之後,還需要再次驗證一下是否存在寫操作,這個驗證操作是通過調用 validate(stamp) 來實作的。

還有一個巧妙的地方:如果執行樂觀讀操作的期間,存在寫操作,會把樂觀讀更新為悲觀讀鎖。這個做法挺合理的,否則你就需要在一個循環裡反複執行樂觀讀,直到執行樂觀讀操作的期間沒有寫操作(隻有這樣才能保證 x 和 y 的正确性和一緻性),而循環讀會浪費大量的 CPU。更新為悲觀讀鎖,代碼簡練且不易出錯。

【漫畫】讀寫鎖ReadWriteLock還是不夠快?再試試StampedLock!資料庫中的鎖StampedLock總結

鎖的更新

在上一篇讀寫鎖文章中,我們說到鎖的更新和降級,ReadWriteLock是隻允許降級而不允許更新的,而StampedLock 支援鎖的降級(通過 tryConvertToReadLock() 方法實作)和更新(通過 tryConvertToWriteLock() 方法實作),讀鎖居然也可以更新為寫鎖,這也是它差別于讀寫鎖的一大特性!

// 讀鎖更新成寫鎖
    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);
        }
    }           

StampedLock 使用注意事項

StampedLock真有這麼完美嗎?挑刺時間又來咯!

1、StampedLock 在命名上并沒有增加 Reentrant,顯然,StampedLock 不支援重入。這個是在使用中必須要特别注意的。

2、StampedLock 的悲觀讀鎖、寫鎖都不支援條件變量(Condition),這個也需要你注意。

3、使用 StampedLock 一定不要調用中斷操作,即不要調用interrupt() 方法,因為内部實作裡while循環裡面對中斷的處理有點問題。如果需要支援中斷功能,一定使用可中斷的悲觀讀鎖 readLockInterruptibly() 和寫鎖 writeLockInterruptibly()。

總結

為起點,我們初始了鎖的概念,了解了synchronized鎖模型,之後又走進了J.U.C Lock包,首先接觸到了

,由于互斥鎖在讀多寫少場景的效率不高,是以接觸了

,而今天,又學習了一種比讀寫鎖還要快的鎖StampedLock。說明JAVA真是博大精深,連鎖都有那麼多種,需要根據實際情況合理選擇才是!

關于StampedLock,重點應該了解它獨特的思想:樂觀的思想。就像人一樣,不能總是悲觀思想,樂觀思想積極面對生活效率才更高!StampedLock通過一個叫做stamp的類似于資料庫版本号的字段,實作了樂觀讀。當然永遠樂觀也是不行的,StampedLock也有它的缺陷,對于這些,你也需要特别注意。