天天看點

【Java并發工具類】StampedLock:比讀寫鎖更快的鎖

前言

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的子集,是以在使用時,還是需要注意一些地方:

  1. StampedLock在命名上沒有增加

    Reentrant

    ,是以,猜想StampedLock不支援重入。事實上,确實如此,StampedLock是不支援重入的。
  2. StampedLock的悲觀讀鎖、寫鎖都不支援條件變量。
  3. 如果線程阻塞在 StampedLock 的

    readLock()

    或者

    writeLock()

    上時,調用該阻塞線程的

    interrupt()

    方法,會導緻 CPU 飙升。(代碼來自參考[1])
    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

每天進步一點點,不要停止前進的腳步~

上一篇: ESXI
下一篇: VM虛拟機?