溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java并發(fā)中如何搞懂讀寫鎖

發(fā)布時間:2021-11-10 13:34:53 來源:億速云 閱讀:151 作者:柒染 欄目:開發(fā)技術

本篇文章為大家展示了Java并發(fā)中如何搞懂讀寫鎖,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

    ReentrantReadWriteLock

    我們來探討一下java.concurrent.util包下的另一個鎖,叫做ReentrantReadWriteLock,也叫讀寫鎖。

    實際項目中常常有這樣一種場景:

    Java并發(fā)中如何搞懂讀寫鎖

    比如有一個共享資源叫做Some Data,多個線程去操作Some Data,這個操作有讀操作也有寫操作,并且是讀多寫少的,那么在沒有寫操作的時候,多個線程去讀Some Data是不會有線程安全問題的,因為線程只是訪問,并沒有修改,不存在競爭,所以這種情況應該允許多個線程同時讀取Some Data。

    但是若某個瞬間,線程X正在修改Some Data的時候,那么就不允許其他線程對Some Data做任何操作,否則就會有線程安全問題。

    那么針對這種讀多寫少的場景,J.U.C包提供了ReentrantReadWriteLock,它包含了兩個鎖:

    • ReadLock:讀鎖,也被稱為共享鎖

    • WriteLock:寫鎖,也被稱為排它鎖

    下面我們看看,線程如果想獲取讀鎖,需要具備哪些條件:

    • 不能有其他線程的寫鎖沒有寫請求;

    • 或者有寫請求,但調用線程和持有鎖的線程是同一個

    再來看一下線程獲取寫鎖的條件:

    • 必須沒有其他線程的讀鎖

    • 必須沒有其他線程的寫鎖

    這個比較容易理解,因為寫鎖是排他的。

    來看下面一段代碼:

    public class ReentrantReadWriteLockTest {
        private Object data;
        //緩存是否有效
        private volatile boolean cacheValid;
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public void processCachedData() {
            rwl.readLock().lock();
            //如果緩存無效,更新cache;否則直接使用data
            if (!cacheValid) {
                //獲取寫鎖前必須釋放讀鎖
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                if (!cacheValid) {
                    //更新數據
                    data = new Object();
                    cacheValid = true;
                }
                //鎖降級,在釋放寫鎖前獲取讀鎖
                rwl.readLock().lock();
                //釋放寫鎖,依然持有讀鎖
                rwl.writeLock().unlock();
            }
            // 使用緩存
            // ...
            // 釋放讀鎖
            rwl.readLock().unlock();
        }
    }

    這段代碼演示的是獲取緩存的時候,判斷緩存是否過期,如果已經過期就更新緩存,如果沒有過期就使用緩存。
    可以看到我們先創(chuàng)建了一個讀鎖,判斷如果緩存有效,就可以使用緩存,使用完之后再把讀鎖釋放。如果緩存無效,就更新緩存執(zhí)行寫操作,所以先把讀鎖給釋放掉,然后創(chuàng)建一個寫鎖,最后更新緩存,更新完緩存后又重新獲取了一個讀鎖并釋放掉寫鎖。

    從這段代碼里可以看出來,一個線程在拿到寫鎖之后它還可以繼續(xù)獲得一個讀鎖。

    小結

    我們來總結一下ReentrantReadWriteLock的三個特性:

    • 公平性

    ReentrantReadWriteLock也可以在初始化時設置是否公平。

    • 可重入性

    讀鎖以及寫鎖也是支持重入的,比如一個線程拿到寫鎖后,他依然可以繼續(xù)拿寫鎖,同理讀鎖也可以。

    • 鎖降級

    要想實現鎖降級,只需要先獲得寫鎖,再獲得讀鎖,最后釋放寫鎖,就可以把一個寫鎖降級為讀鎖了。但是一個讀鎖是沒有辦法升級為寫鎖的。

    最后我們來對比一下ReentrantLock與ReentrantReadWriteLock

    • ReentrantLock:完全互斥

    • ReentrantReadWriteLock:讀鎖共享,寫鎖互斥

    因此在讀多寫少的場景下,ReentrantReadWriteLock的性能、吞吐量各方面都會比ReentrantLock要好很多。但是對于寫多的場景ReentrantReadWriteLock就不那么明顯了。

    StampedLock

    上面我們已經探討了ReentrantReadWriteLock能夠大幅度提升讀多寫少場景下的性能,StampedLock是在JDK8引入的,可以認為這是一個ReentrantReadWriteLock的增強版。

    那么大家想,既然有了ReentrantReadWriteLock,為什么還要搞一個StampedLock呢?

    這是因為ReentrantReadWriteLock在一些特定的場景下存在問題。

    比如寫線程的“饑餓”問題。
    舉個例子:假設現在有超級多的線程在操作ReentrantReadWriteLock,執(zhí)行讀操作的線程超級多,而執(zhí)行寫操作的線程很少,而如果這個執(zhí)行寫操作的線程想要拿到寫鎖,而ReentrantReadWriteLock的寫鎖是排他的,要想拿到寫鎖就意味著其他線程不能有讀鎖也不能有寫鎖,所以在讀線程超級多,寫線程超級少的情況下就容易造成寫線程饑餓問題,也就是說,執(zhí)行寫操作的線程可能一直搶不到鎖,即使可以把公平性設置為true,但是這樣又會導致性能的下降。

    那么我們看看StampedLock怎么玩:

    首先,所有獲取鎖的方法都會返回stamp,它是一個數字,如果stamp=0說明操作失敗了,其他的值表示操作成功。

    其次就是所有獲取鎖的方法,需要用stamp作為參數,參數的值必須和獲得鎖時返回的stamp一致。

    其中StampedLock提供了三種訪問模式:

    • Writing模式:類似于ReentrantReadWriteLock的寫鎖R

    • eding(悲觀讀模式):類似于ReentrantReadWriteLock的讀鎖。

    • Optimistic reading:樂觀讀模式

    悲觀讀模式:在執(zhí)行悲觀讀的過程中,不允許有寫操作

    樂觀讀模式:在執(zhí)行樂觀讀的過程中,允許有寫操作

    通過介紹我們可以發(fā)現,StampedLock中的悲觀讀與樂觀讀和我們操作數據庫中的悲觀鎖、樂觀鎖有一定的相似之處。

    此外StampedLock還提供了讀鎖和寫鎖相互轉換的功能:

    我們知道ReentrantReadWriteLock的寫鎖是可以降級為讀鎖的,但是讀鎖沒辦法升級為寫鎖,而StampedLock它提供了讀鎖和寫鎖之間互相轉換的功能。

    最后,StampedLock是不可重入的,這也是和ReentrantReadWriteLock的一個區(qū)別。

    讀過源碼的同學可能知道,在StampedLock源碼里有一段注釋:

    Java并發(fā)中如何搞懂讀寫鎖

    我們來看一下這段注釋,他寫的非常經典,演示了StampedLock API如何使用。

    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();
          // 假設(x,y)=(10,10)
          // 但是這是一個樂觀讀鎖,(x,y)可能被其他線程修改為(20,20)
          double currentX = x, currentY = y;
          //因此這里要驗證獲得樂觀鎖后,有沒有發(fā)生寫操作
          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);
          }
        }
    }

    Java并發(fā)中如何搞懂讀寫鎖

    這個類有三個方法,move方法用來移動一個點的坐標,instanceFromOrigin用來計算這個點到原點的距離,moveIfAtOrigin表示當這個點位于原點的時候用來移動這個點的坐標。

    我們來分析一下源碼:

    move方法是一個純粹的寫操作,在操作之前添加寫鎖,操作結束釋放寫鎖;

    instanceOrigin首先獲得一個樂觀鎖,然后開始讀數據,我們假設(x,y)=(10,10),但是這是一個樂觀讀鎖,(x,y)可能被其他線程修改為(20,20),所以他會驗證獲得樂觀鎖后,有沒有發(fā)生寫操作,如果validate結果為true的話,表示沒有發(fā)生過寫操作,如果發(fā)生過寫操作,那么就會改用悲觀讀鎖重讀數據,然后計算結果,當然最后要把鎖釋放掉。

    最后moveIfAtOrigin方法也比較簡單,主要演示了怎么從悲觀讀鎖轉換成寫鎖。

    StampedLock主要通過樂觀讀的方式提升性能,同時也解決了寫線程的饑餓問題,但是有得必有失,我們從示例代碼中不難看出,StampedLock使用起來要比ReentrantReadWriteLock復雜很多,所以使用者要在性能和復雜度之間做一個取舍。

    上述內容就是Java并發(fā)中如何搞懂讀寫鎖,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業(yè)資訊頻道。

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI