ReentrantReadWriteLock 源碼解析

 

一、讀寫鎖簡介

   現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。

 針對這種場景,JAVA的併發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖

類圖如下:

類圖

說明:如上圖所示Sync爲ReentrantReadWriteLock內部類,Sync繼承自AQS、NonfairSync繼承自Sync類、FairSync繼承自Sync類(通過構造函數傳入的布爾值決定要構造哪一種Sync實例);ReadLock實現了Lock接口、WriteLock也實現了Lock接口;

AQS定義了獨佔模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.還定義了抽象方法tryAcquire()、tryAcquiredShared()、tryRelease()和tryReleaseShared()由子類實現,tryAcquire()和tryAcquiredShared()分別對應獨佔模式和共享模式下的鎖的嘗試獲取,就是通過這兩個方法來實現公平性和非公平性,在嘗試獲取中,如果新來的線程必須先入隊才能獲取鎖就是公平的,否則就是非公平的。這裏可以看出AQS定義整體的同步器框架,具體實現放手交由子類實現。

通過類圖我們知道一些核心操作由Sync類實現

Sync類內部存在兩個內部類,分別爲HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要與讀鎖配套使用;

Sync源碼如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
        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; }
      
         // 計數器
       static final class HoldCounter {
           // 計數
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            // 獲取當前線程的TID屬性的值
            final long tid = getThreadId(Thread.currentThread());
        }
        // 本地線程計數器
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            // 重寫初始化方法,在沒有進行set的情況下,獲取的都是該HoldCounter值
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
        // 本地線程計數器
        private transient ThreadLocalHoldCounter readHolds;
        // 緩存的計數器
        private transient HoldCounter cachedHoldCounter;
        /記錄第一個持有共享鎖線程的持有共享鎖的數量,作者認爲大多數情況下不會有併發,更多的是線程交替持有鎖
        private transient Thread firstReader = null;
         // 第一個讀線程的計數
        private transient int firstReaderHoldCount;
	      //構造器
        Sync() {
             // 本地線程計數器
            readHolds = new ThreadLocalHoldCounter();
            // 設置AQS的狀態
            setState(getState()); // ensures visibility of readHolds
        }
       }

 


直接上源碼


一.寫鎖過程

獲取寫鎖:

    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	ReentrantReadWriteLock.WriteLock writeLock =  readWriteLock.writeLock();
        writeLock.lock();
        //ReentrantReadWriteLock 內部類Sync 繼承自AQS,這裏是調用aqs中的acquire方法
        public void lock() {
          sync.acquire(1);
        }
        //AQS中定義了tryAcquire抽象方法,具體的實現由子類去實現
	      //這裏除tryAcquire方法和Reentrantlock 略有不同,後續操作一樣一樣的,
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

說明:除了aqs中的tryAcquire由具體的實現類來實現,其他部分和ReetrantLock獲取鎖的過程一樣的,這裏就不絮叨了,下邊主要看下tryAcquire方法的具體實現。(可以參考我寫的這篇ReentrantLock詳解,或者不清楚的直接留言我)

        protected final boolean tryAcquire(int acquires) {
            
            Thread current = Thread.currentThread();
            //獲取當前鎖對象狀態
            int c = getState();
            // 返回count中表示排它鎖的數量 
            int w = exclusiveCount(c);
            //說明鎖被佔有(共享鎖或者排它鎖)
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //說明現在有共享鎖被別的線程佔有,嘗試獲取鎖失敗
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //說明當前線程持有排它鎖或者共享鎖,這裏是判斷有沒有超出重入次數
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 排它鎖重入,直接獲取鎖
                setState(c + acquires);
                return true;
            }
                //如果當前爲非公平鎖: writerShouldBlock 方法直接返回 false,然後去爭搶鎖
                /**如果當前爲公平的寫鎖   writerShouldBlock 該方法調動  AQS的hasQueuedPredecessors  方法,
                判斷當前同步隊列有沒有等待的線程,如果有返回true,沒有等待的線程在返回false 然後去爭搶鎖**/
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //成功獲取鎖,把當錢鎖設置爲當前線程佔有
            setExclusiveOwnerThread(current);
            return true;
        }

這裏獲取鎖失敗的情況主要有

  1. 鎖被其他線程佔有(共享鎖或者排它鎖)
  2. 如果爲公平鎖,等待隊列上有線程等待
  3. 超出鎖重入最大數量
  4. 別的線程爭搶了鎖

失敗後的具體操作見ReentrantLock詳解

寫鎖的釋放

        ReentrantReadWriteLock.WriteLock writeLock =  readWriteLock.writeLock();
        writeLock.unlock();
        public void unlock() {
          sync.release(1);
        }
        //AQS中定義了tryAcquire抽象方法,具體的實現由子類去實現
        public final boolean release(int arg) {
        //tryRelease嘗試釋放鎖(鎖status-arg),如果當前線程沒有佔有的鎖(鎖status=0)  返回true
        if (tryRelease(arg)) {
            //當前線程釋放掉了所有鎖
            Node h = head;
            //如果等待隊列第一個結點有掛起的線程,將它喚醒去爭搶
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
        protected final boolean tryRelease(int releases) {
            //判斷當前線程是否爲持有鎖的線程
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            //判斷是否已經全部釋放寫鎖
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

讀鎖的獲取和釋放


讀鎖獲取過程:

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
		ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
		readLock.lock();
        public void lock() {
            sync.acquireShared(1);
        }

         public final void acquireShared(int arg) {
            if (tryAcquireShared(arg) < 0)
                doAcquireShared(arg);
        }

主要來看下aqs定義的抽象方法tryAcquireShared (sync具體實現的)

獲取讀鎖失敗的情況有 :
        (1)有其他線程持有排它鎖,獲取鎖失敗。
        (2)公平鎖:同步隊列有等待節點;非公平鎖:同步隊列頭節點爲排它鎖同步隊列(防止寫鎖飢餓)
        (3)讀鎖數量達到最多,拋出異常。

 除了以上三種情況,該線程會循環嘗試獲取讀鎖直到成功。

        protected final int tryAcquireShared(int unused) {
            
            Thread current = Thread.currentThread();
            int c = getState();
            //如果當前排他鎖被佔有,判斷是不是當前線程佔有的
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //共享鎖佔有的數量
            int r = sharedCount(c);
            //readerShouldBlock 方法 判斷同步隊列中第一個節點是 什麼狀態
	        //如果是公平鎖:同步隊列有節點就返回true,有可能是共享鎖也有可能是排它鎖的節點,
            //如果是非公平鎖:同步隊列第一個節點是等待排它鎖  就返回true,防止排它鎖出現飢餓狀態
            //readerShouldBlock 爲false就直接獲取鎖
            if (!readerShouldBlock() &&
                 //c +的是  1<<16,讀鎖爲高16位表示
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                 //r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
                if (r == 0) {
                    // 設置第一個讀線程
                    firstReader = current;
                    // 讀線程佔用的資源數爲1
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {// 當前線程爲第一個讀線程,表示第一個讀鎖線程重入
                    // 佔用資源數加1
                    firstReaderHoldCount++;
                } else {
                    // 獲取計數器
                    //如果共享鎖是被第2+n個線程佔有,則使用threadlocal 記錄每個線程持有的線程數量
                    HoldCounter rh = cachedHoldCounter;
                     // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid
                    if (rh == null || rh.tid != getThreadId(current))
                    // 獲取當前線程對應的計數器
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0) // 計數爲0
                     //加入到readHolds中
                        readHolds.set(rh);
                        //計數+1
                    rh.count++;
                }
                return 1;
            }
            //獲取鎖失敗,放到循環裏重試
            return fullTryAcquireShared(current);
        }

        final int fullTryAcquireShared(Thread current) {
         
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //有線程持有寫鎖,且該線程不是當前線程,獲取鎖失敗
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    else{
                            //有線程持有寫鎖,且該線程是當前線程,則應該放行讓其重入獲取鎖,否則會造成死鎖
                        }
                //沒有線程持有排它鎖,判斷獲取共享鎖是否應該被阻塞
                //readerShouldBlock 方法 判斷同步隊列中第一個節點是 什麼狀態
	            //如果是公平鎖:同步隊列有節點就返回true,有可能是共享鎖也有可能是排它鎖的節點,
                //如果是非公平鎖:同步隊列第一個節點是等待排它鎖  就返回true,防止排它鎖出現飢餓狀態
                //readerShouldBlock 爲false就直接獲取鎖
                } else if (readerShouldBlock()) {
                    // 確保獲取的不是  讀重入鎖
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        //如果當前鎖不是 讀讀重入,且應該阻塞,那麼獲取鎖失敗
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //判斷當前線程有沒有超過最大數量限制
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //再次嘗試獲取鎖~~(可能爲寫讀重入)
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

讀鎖獲取失敗,調用aqs的doAcquireShared方法嘗試將當前線程任務節點加入到同步隊列中(加入同步隊列的具體細節見ReentrantLock詳解

private void doAcquireShared(int arg) {
        // 將當前線程任務添加到同步隊列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //  獲取當前節點的前繼節點 
                final Node p = node.predecessor();
                //  判斷前繼節點是否是head節點
                if (p == head) {
                     //如果前置節點爲head說明,他當前線程是等待隊列中的第一個,那麼就嘗試獲取鎖(這裏可能是避免線程的上下文切換)
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                         // 獲取 lock 成功, 設置新的 head, 並喚醒後繼獲取  readLock 的節點
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        // 該線程有可能是被中斷喚醒,也有可能是被其他線程喚醒,這裏設置下中斷狀態
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //普通鎖的情況下:,然後返回false繼續自旋  嘗試獲取鎖
                //shouldParkAfterFailedAcquire  只有發現當前節點不是首節點纔會返回true ,然後掛起當前線程,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果該線程是被中斷喚醒的,用於輔助後續操作判斷當前線程是被中斷喚醒的
                    interrupted = true;
            }
        } finally {
            //如果該方法因爲某些特殊情況意外的退出(沒有獲取鎖就退出了),那麼就取消嘗試獲取鎖
            if (failed)
                cancelAcquire(node);
        }
    }

如果讀鎖獲取失敗後,嘗試將當前線程節點加入到同步隊列中。

如果該節點爲頭節點,那麼就自旋爭搶鎖(避免上下文切換),獲取鎖成功的話,調用setHeadAndPropagate方法繼續喚醒後續節點(如果後續節點爲讀鎖等待節點的話);

如果該節點不爲頭節點,將當前線程掛起;

我們來看下setHeadAndPropagate方法
  

// 如果讀鎖(共享鎖)獲取成功,或頭部節點爲空,或頭節點取消,或剛獲取讀鎖的線程的下一個節點爲空,或在節點的下個節點也在申請讀鎖,
//則在CLH隊列中傳播下去喚醒線程
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);

        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
            //下面具體分析
                doReleaseShared();
        }
    }

注:後續讀鎖的釋放操作也調用的這個doReleaseShared 方法

private void doReleaseShared() {
        for (;;) {
            //喚醒操作由頭結點開始,注意這裏的頭節點已經是上面新設置的頭結點了
            //其實就是喚醒上面新獲取到共享鎖的節點的後繼節點
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示後繼節點需要被喚醒
                if (ws == Node.SIGNAL) {
                    //這裏需要控制併發,因爲入口有setHeadAndPropagate跟releaseShared兩個,避免兩次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //執行喚醒操作      
                    unparkSuccessor(h);
                }
                //如果後繼節點暫時不需要喚醒,則把當前節點狀態設置爲PROPAGATE
                (這裏不是很明白,爲什麼不需要喚醒的節點要設置這個狀態,哪個老鐵知道爲什麼的話指點下)
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;
            }
            //如果頭結點沒有發生變化,表示設置完成,退出循環
            //如果頭結點發生變化,比如說其他線程獲取到了鎖,爲了使自己的喚醒動作可以傳遞,必須進行重試
            if (h == head)                   
                break;
        }

怎麼理解這個傳播呢:
    就是隻要獲取成功到讀鎖,那就要傳播到下一個節點(如果一下個節點繼續是讀鎖的申請,只要成功獲取,就再下一個節點,直到隊列尾部或爲寫鎖的申請,停止傳播)。


讀鎖的釋放過程

    ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        readLock.unlock();

    public void unlock() {
       sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            //上文有講
            doReleaseShared();
            return true;
        }
        return false;
    }

    //該方法的主要作用就是用來維護下當前線程讀鎖的重入數量;
    //如果沒有線程佔有讀鎖,就返回true 喚醒後續節點
     protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            //保證一定能釋放掉讀鎖的佔有
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

讀鎖的釋放過程比較簡單,這裏就不做過多的解釋了

這裏思考一個問題,獲取讀鎖的時候我們講到讀鎖傳播的概念,爲什麼在讀鎖釋放的時候,如果還有別的線程佔有讀鎖就不用傳播了呢?

因爲在現在獲取讀鎖的時候 已經完成讀線程喚醒的傳播了~~


總結:

獲取寫鎖:獲取寫鎖的過程總體和ReentrantLock詳解流程一樣;

  1. 嘗試獲取寫鎖(如果沒有線程佔有鎖直接獲取成功,並把當前鎖設爲獨佔)
    1. 公平鎖:先判斷同步隊列中是否有等待節點在等待獲取鎖

    2. 非公平鎖:上來就直接爭搶鎖

  2. 如果有其他現在佔有鎖(讀鎖或者寫鎖),獲取失敗,加入到同步隊列中
  3. 判斷當前節點是否爲頭節點,
    1. 是:自旋爭搶鎖(避免上下文切換)
    2. 否:調用LockSupport.park 方法掛起當前線程,等待被喚醒(別的線程調用LockSupport.uppark 或者被中斷  或者 超時)

寫鎖的釋放:

  1. 判斷當前鎖是否爲釋放鎖的線程佔有
  2. 設置當前鎖狀態,如果所有重入鎖都釋放掉了,設置當前鎖獨佔爲null,
  3. 喚醒同步隊列中的頭節點

注:寫鎖的釋放並沒有進行讀鎖的釋放傳播,讀鎖的傳播是有讀鎖成功獲取讀鎖以後進行的

讀鎖的獲取:

  1. 判斷寫鎖是否被佔用(是:判斷是否爲寫讀重入鎖)
    1. 是:判斷是否爲寫讀重入鎖
      1. 否:獲取失敗,加入同步隊列中
  2. 判斷當前讀線程是否應該被阻塞
    1. 公平鎖:如果同步隊列中有等待節點就獲取鎖失敗,把當前讀線程節點加入到同步隊列
    2. 非公平鎖:如果同步隊列中的第一個節點爲寫鎖的等待節點獲取鎖失敗,把當前讀線程節點加入到同步隊列(防止寫鎖飢餓)
  3. 獲取讀鎖成功,維護線程佔有讀鎖的數量,判斷當前節點是否爲第一個獲取讀鎖的線程:
    1. 否:把當前線程佔有讀鎖數量維護進入HoldCounter(繼承自ThreadLocal,爲每個線程都維護了一個讀鎖重入的計數)
    2. 是:直接通過變量firstReader,firstReaderHoldCount維護當前線程佔有讀鎖的數量(這裏作者應該是認爲大多數情況下鎖的獲取爲交替獲取,沒必要直接就用線程計數器來爲每個線程爲一個數量)
    3. 注:如果後續節點爲讀鎖節點,就喚醒(該行爲會傳播)
  4. 獲取鎖失敗:加入到同步隊列中,判斷當前節點是否頭節點
    1. 是:自旋爭搶鎖
    2. 否:掛起當前線程,等待被喚醒

讀鎖的釋放:

  1. 設置當前線程佔有的鎖數量-1
  2. 喚醒後續節點~~~

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章