java併發編程(十八)啥是讀寫鎖ReentrantReadWriteLock? 一、初識ReentrantReadWriteLock 二、使用案例 三、源碼分析

前面我們學習了AQS,ReentrantLock等,現在來學習一下什麼是讀寫鎖ReentrantReadWriteLock。

當讀操作遠遠高於寫操作時,這時候可以使用【讀寫鎖】讓【讀-讀】可以併發,提高性能。

本文還是基於源碼的形式,希望同學們能夠以本文爲思路,自己跟蹤源碼一步步的debug進去,加深理解。

一、初識ReentrantReadWriteLock

同樣的,先看下其類圖:

  • 實現了讀寫鎖接口ReadWriteLock
  • 有5個內部類,與ReentrantLock相同的是FairSyncNonfairSyncSync,另外不同的是增加兩個內部類,都實現了Lock接口:
    • WriteLock
    • ReadLock
  • Sync 增加了兩個內部類 :
    • HoldCounter:持有鎖的計數器
    • ThreadLocalHoldCounter :維護HoldCounter的ThreadLocal

二、使用案例

通常會維護一個操作數據的容器類,內部應該封裝好數據的read和write方法,如下所示:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @description: 數據容器類
 * @author:weirx
 * @date:2022/1/13 15:29
 * @version:3.0
 */
public class DataContainer {

    /**
     * 初始化讀鎖和寫鎖
     */
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    protected void read(){
        readLock.lock();
        try {
            System.out.println("獲取讀鎖");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println("釋放讀鎖");
        }
    }

    protected void write(){
        writeLock.lock();
        try {
            System.out.println("獲取寫鎖");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println("釋放寫鎖");
        }
    }
}

簡單測試一下,分爲讀讀、讀寫、寫寫。

  • 讀讀:
    public static void main(String[] args) {
        //初始化數據容器
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.read();
        }, "t2").start();
    }

結果,讀讀不互斥,同時獲取讀鎖,同時釋放:

獲取讀鎖
獲取讀鎖
釋放讀鎖
釋放讀鎖
  • 讀寫:
    public static void main(String[] args) {
        //初始化數據容器
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }

結果,讀寫互斥,無論是先執行read還是write方法,都會等到讀鎖或寫鎖被釋放之後,纔會獲取下一把鎖:

獲取讀鎖 -- 第一個執行
釋放讀鎖 -- 第二個執行
獲取寫鎖 -- 第三個執行
釋放寫鎖 -- 第四個執行
  • 寫寫:
    public static void main(String[] args) {
        //初始化數據容器
        DataContainer dataContainer = new DataContainer();

        new Thread(() -> {
            dataContainer.write();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }

結果,寫寫互斥,只有第一把寫鎖釋放後,才能獲取下一把寫鎖:

獲取寫鎖
釋放寫鎖
獲取寫鎖
釋放寫鎖

注意:

  • 鎖重入時,持有讀鎖再去獲取寫鎖,會導致寫鎖一直等待
        protected void read(){
          readLock.lock();
          try {
              System.out.println("獲取讀鎖");
              TimeUnit.SECONDS.sleep(1);
              System.out.println("獲取寫鎖");
              writeLock.lock();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              readLock.unlock();
              System.out.println("釋放讀鎖");
          }
      }
    
    結果:不會釋放
    獲取讀鎖
    獲取寫鎖
    
  • 鎖重入時,持有寫鎖,可以再去獲取讀鎖。
     protected void write(){
          writeLock.lock();
          try {
              System.out.println("獲取寫鎖");
              TimeUnit.SECONDS.sleep(1);
              System.out.println("獲取讀鎖");
              readLock.lock();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              writeLock.unlock();
              System.out.println("釋放寫鎖");
          }
      }
    
    結果:
    獲取寫鎖
    獲取讀鎖
    釋放寫鎖
    

三、源碼分析

我們根據前面的例子,從讀鎖的獲取到釋放,從寫鎖的獲取到釋放,依次查看源碼。

先注意一個事情,讀寫鎖是以不同的位數來區分獨佔鎖和共享鎖的狀態的:

       /*
         * 讀和寫分爲上行下兩個部分,低16位是獨佔鎖狀態,高16位是共享鎖狀態
         */

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** 返回以count表示的共享持有數 */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 返回以count表示的互斥保持數  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

3.1 讀鎖分析

3.1.1 讀鎖獲取

從 readLock.lock(); 這裏進入分析過程:

        /**
        * 獲取讀鎖。
        * 如果寫鎖沒有被另一個線程持有,則獲取讀鎖並立即返回。
        * 如果寫鎖被另一個線程持有,那麼當前線程將被禁用以用於線程調度目的並處於休眠狀態,直到獲得讀鎖爲止
        */
        public void lock() {
            sync.acquireShared(1);
        }

如上的lock方法,是ReentrantReadWriteLock子類ReadLock的方法,而acquireShared方法是在AQS的子類Syn當中定義的,這個方法嘗試以共享的方式獲取讀鎖,失敗則進入等待隊列, 不斷重試,直到獲取讀鎖爲止。

    public final void acquireShared(int arg) {
        // 被其他線程持有的話,就走AQS的doAcquireShared
        if (tryAcquireShared(arg) < 0)
            // 獲取共享鎖,失敗加入等待隊列,不可中斷的獲取,直到獲取爲止
            doAcquireShared(arg);
    }

tryAcquireShared是在ReentrantReadWriteLock當中實現的,我們直接看代碼:

        protected final int tryAcquireShared(int unused) {
            // 獲取當前線程
            Thread current = Thread.currentThread();
            // 獲取當前鎖狀態
            int c = getState();
            // 獨佔鎖統計不等於0 且 持有者不是當前線程,就返回 -1 ,換句話說,被其他線程持有
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 共享鎖數量
            int r = sharedCount(c);
            // 返回fase纔有資格獲取讀鎖
            if (!readerShouldBlock() &&
                // 持有數小於默認值
                r < MAX_COUNT &&
                // CAS 設置鎖狀態
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 持有共享鎖爲0
                if (r == 0) {
                    // 第一個持有者是當前線程
                    firstReader = current;
                    // 持有總數是 1 
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    // 持有鎖的是當前線程本身,就把技術 + 1
                    firstReaderHoldCount++;
                } else {
                    // 獲取緩存計數
                    HoldCounter rh = cachedHoldCounter;
                    // 如果是null 或者 持有線程的id不是當前線程
                    if (rh == null || rh.tid != getThreadId(current))
                        // 賦值給緩存
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        // rh不是null ,且是當前線程,就把讀鎖持有者設爲緩存中的值
                        readHolds.set(rh);
                    // 將其 + 1
                    rh.count++;
                }
                return 1;
            }
            // 想要獲取讀鎖的線程應該被阻塞,保底工作,處理 CAS 未命中和在 tryAcquireShared 中未處理的重入讀取
            return fullTryAcquireShared(current);
        }

從上面的源碼我們可以看得出來,寫鎖和讀鎖之間是互斥的。

3.1.2 讀鎖釋放

直接看關鍵部分

    /**
      * 以共享模式釋放鎖,tryReleaseShared返回true,則釋放
      */
    public final boolean releaseShared(int arg) {
        // 釋放鎖
        if (tryReleaseShared(arg)) {
            // 喚醒隊列的下一個線程
            doReleaseShared();
            return true;
        }
        return false;
    }

看看讀寫鎖的tryReleaseShared實現:

        protected final boolean tryReleaseShared(int unused) {
            //。。。省略。。。
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // 讀鎖的計數不會影響其它獲取讀鎖線程, 但會影響其它獲取寫鎖線程
                    // 計數爲 0 纔是真正釋放
                    return nextc == 0;
            }
        }

如果上述方法釋放成功,則走下面AQS繼承來的方法:

    private void doReleaseShared() {
        // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一個節點 unpark
        // 如果 head.waitStatus == 0 ==> Node.PROPAGATE
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
              // 如果有其它線程也在釋放讀鎖,那麼需要將 waitStatus 先改爲 0
              // 防止 unparkSuccessor 被多次執行
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; // loop to recheck cases
                    unparkSuccessor(h);
                }
                // 如果已經是 0 了,改爲 -3,用來解決傳播性
                else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue; // loop on failed CAS
            }
            if (h == head) // loop if head changed
                break;
        }
    }

3.2 寫鎖分析

3.2.1 獲取鎖

    public final void acquire(int arg) {
        // 嘗試獲得寫鎖失敗
        if (!tryAcquire(arg) &&
                        // 將當前線程關聯到一個 Node 對象上, 模式爲獨佔模式
                        // 進入 AQS 隊列阻塞
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) {
            selfInterrupt();
        }
    }

讀寫鎖的上鎖方法:tryAcquire

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
           // 獲得低 16 位, 代表寫鎖的 state 計數
            int w = exclusiveCount(c);
            if (c != 0) {
                // 如果寫鎖是0 或者 當前線程不等於獨佔線程,獲取失敗
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 寫鎖計數超過低 16 位, 報異常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 寫鎖重入, 獲得鎖成功
                setState(c + acquires);
                return true;
            }
            // 寫鎖應該阻塞
            if (writerShouldBlock() ||
                //    更改計數失敗  
                !compareAndSetState(c, c + acquires))
                // 獲取鎖失敗
                return false;
            // 設置當前線程獨佔鎖
            setExclusiveOwnerThread(current);
            return true;
        }

3.2.2 釋放鎖

release:

    public final boolean release(int arg) {
        // 嘗試釋放寫鎖成功
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease:

    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        // 因爲可重入的原因, 寫鎖計數爲 0, 纔算釋放成功
        boolean free = exclusiveCount(nextc) == 0;
        if (free) {
            setExclusiveOwnerThread(null);
        }
        setState(nextc);
        return free;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章