1. 程式人生 > >【搞定Java併發程式設計】第20篇:讀寫鎖 --- ReentrantReadWriteLock詳解

【搞定Java併發程式設計】第20篇:讀寫鎖 --- ReentrantReadWriteLock詳解

上一篇:重入鎖 --- ReentrantLock 詳解(點選檢視)

本文目錄

1、讀寫鎖的概述

2、讀寫鎖的具體實現

2.1、讀寫狀態的設計

2.2、鎖的獲取

2.2.1、寫鎖的獲取

2.2.2、讀鎖的獲取

2.3、鎖的釋放

2.3.1、寫鎖的釋放

2.3.2、讀鎖的釋放

3、ReentrantReadWriteLock中的其他方法

3.1、getOwner()

3.2、getReadLockCount()

3.3、getReadHoldCount()

3.4、getWriteHoldCount()

4、總結


推薦幾篇不錯的文章:

1、輕鬆掌握java讀寫鎖(ReentrantReadWriteLock)的實現原理:這篇文章用圖解式的方式說明了讀寫鎖的兩種模式下的工作流程(公平讀寫鎖和非公平讀寫鎖),內容簡單易懂,推薦閱讀。

2、深入理解讀寫鎖—ReadWriteLock原始碼分析分析了讀鎖和寫鎖的獲取、讀鎖和寫鎖的釋放的原始碼。

1、讀寫鎖的概述

之前提到的鎖(如:ReentrantLock)基本都是排他鎖,即在同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。

讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大的提升。

一般情況下,讀寫鎖的效能都會比排它鎖好,因為大多數場景讀是多於寫的。在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的併發性和吞吐量。Java併發包中提供讀寫鎖的實現是ReentrantReadWriteLock,它提供的特性如下表所示:

 但是事實上在ReentrantReadWriteLock裡鎖的實現是靠內部java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的。這個類看起來比較眼熟,它是AQS的一個子類,這中類似的結構在CountDownLatch、ReentrantLock、Semaphore裡面都存在。

在ReentrantReadWriteLock裡面的鎖主體就是一個Sync,也就是FairSync或者NonfairSync,所以說實際上只有一個 
鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣而已。

先看下ReadWriteLock介面的原始碼:

public interface ReadWriteLock {
    
    // 讀鎖
    Lock readLock();

    // 寫鎖
    Lock writeLock();
}

ReentrantReadWriteLock類是ReadWriteLock的實現類,它的整體結果如下:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    
    private final ReentrantReadWriteLock.ReadLock readerLock;
   
    private final ReentrantReadWriteLock.WriteLock writerLock;
   
    final Sync sync;

    public ReentrantReadWriteLock() {
        this(false);   // 預設是非公平鎖
    }

    // 公平鎖和非公平鎖在ReentrReentrantReadWriteLock初始化的時候就確定了
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    // 實現父介面ReadWriteLock介面中的方法
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

    // 靜態內部類Sync,它的父類是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
		// ...
        // 當別的執行緒獲取讀鎖時,是否要阻塞
        abstract boolean readerShouldBlock();

        // 當別的執行緒獲取寫鎖時,是否要阻塞
        abstract boolean writerShouldBlock();
        // ...
    }
	
    // 靜態內部類NonfairSync,其父類是Sync
    static final class NonfairSync extends Sync {
		// ...
    }
	
    // 靜態內部類FairSync,其父類是Sync
    static final class FairSync extends Sync {
		// ...
    }
	
    // 讀鎖,實現了Lock介面
    public static class ReadLock implements Lock, java.io.Serializable {
        
        private final Sync sync;
        // 構造器
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
		// ...
    }
	
    // 寫鎖,實現了Lock介面
    public static class WriteLock implements Lock, java.io.Serializable {
       
        private final Sync sync;
        // 構造器 
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
		// ...
    }

	// ...
}

從上面的的程式碼中可以發現,Sync中提供了很多方法,但是隻有兩個方法是抽象的,也就是需要子類實現的。那我們就來看看它的子類FairSync和NonFairSync是如何實現這兩個抽象方法的。

  • FairSync
static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
}

writerShouldBlock和readerShouldBlock方法都表示當有別的執行緒也在嘗試獲取鎖時,是否應該阻塞。 

對於公平模式,hasQueuedPredecessors()方法表示前面是否有等待執行緒。一旦前面有等待執行緒,那麼為了遵循公平,當前執行緒也就應該被掛起。 

  • NonFairSync
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
}

從上面可以看到,非公平模式下,writerShouldBlock直接返回false,說明不需要阻塞;而readShouldBlock呼叫了apparentlyFirstQueuedIsExcluisve()方法。該方法在當前執行緒是寫鎖佔用的執行緒時,返回true;否則返回false。也就說明,如果當前有一個寫執行緒正在寫,那麼該讀執行緒應該阻塞。

  • ReentrantReadWriteLock有如下特性: 
  • 1、獲取順序 

非公平模式(預設): 

當以非公平初始化時,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取,可能會延緩一個或多個讀或寫執行緒,但是會比公平鎖有更高的吞吐量。 

公平模式 :

當以公平模式初始化時,執行緒將會以佇列的順序獲取鎖。噹噹前執行緒釋放鎖後,等待時間最長的寫鎖執行緒就會被分配寫鎖;或者有一組讀執行緒組等待時間比寫執行緒長,那麼這組讀執行緒組將會被分配讀鎖。 

當有寫執行緒持有寫鎖或者有等待的寫執行緒時,一個嘗試獲取公平的讀鎖(非重入)的執行緒就會阻塞。這個執行緒直到等待時間最長的寫鎖獲得鎖後並釋放掉鎖後才能獲取到讀鎖。 

  • 2、可重入 

允許讀鎖可寫鎖可重入。寫鎖可以獲得讀鎖,讀鎖不能獲得寫鎖。 

  • 3、鎖降級 

允許寫鎖降低為讀鎖 ,但不支援讀鎖升級為寫鎖。

  • 4、中斷鎖的獲取 

在讀鎖和寫鎖的獲取過程中支援中斷。

  • 5、支援Condition 

寫鎖提供Condition實現。

  • 6、監控 

提供確定鎖是否被持有等輔助方法


2、讀寫鎖的具體實現

下面分析ReentrantReadWriteLock的具體實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級。至於公平性與非公平性,在上篇文章重入鎖ReentrantLock中已經講解過了,本質上都是一樣的,這裡不再單獨做區分的講解了。

2.1、讀寫狀態的設計

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個執行緒重複獲取的次數。而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。

如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如下圖所示:

讀寫鎖狀態的劃分方式

上圖所示的當前同步狀態表示一個執行緒已經獲取了寫鎖,期重進入了2次,同時也連續獲取了2次讀鎖。讀鎖和寫鎖通過位運算確定自己讀和寫的狀態。

2.2、鎖的獲取

2.2.1、寫鎖的獲取

寫鎖是一個支援重進入的排它鎖。如果當前執行緒已經獲取了寫鎖,則增加寫狀態。如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀鎖狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態。

  • 第1步:呼叫內部類WriteLock中的lock()方法
public static class WriteLock implements Lock, java.io.Serializable {
    // ...
            
    // 獲取寫鎖
    public void lock() {
        sync.acquire(1);  // 呼叫的是同步器AQS中的方法
    }
    // ...
}
  • 第2步:呼叫AQS中的acquire方法(因為sync繼承了AQS類,所有可以呼叫AQS中開放的方法)
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
            selfInterrupt();
        }         
}
  • 第3步:呼叫Sync中的tryAcquire方法
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    
	// ...
	
    // 靜態內部類Sync,它的父類是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
		 // ...
        protected final boolean tryAcquire(int acquires) {
            // 獲取呼叫lock方法的當前執行緒
            Thread current = Thread.currentThread();
            // 獲取當前執行緒的狀態
            int c = getState();
            // 獲取寫鎖的狀態,寫鎖是排它鎖
            int w = exclusiveCount(c);
            if (c != 0) {
                // 存在讀鎖或者當前獲取執行緒不是已經獲取寫鎖的執行緒,返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 如果寫鎖的個數超過了最大值65535,丟擲異常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 寫入重入鎖,返回true
                setState(c + acquires);
                return true;
            }
            // 如果當前沒有執行緒獲得鎖,如果寫執行緒應該被阻塞或者CAS失敗,返回false
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
            // 否則將當前執行緒置為獲得寫鎖的執行緒,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
		// ...
    }
}

從程式碼和註釋可以看到,獲取寫鎖時有三步: 

1. 如果當前有寫鎖或者讀鎖。如果只有讀鎖,返回false,因為這時如果可以寫,那麼讀執行緒得到的資料就有可能錯誤;如果有寫鎖,但是執行緒不同,即不符合寫鎖重入規則,返回false ;

2. 如果寫鎖的數量將會超過最大值65535,丟擲異常;否則,寫鎖重入 ;

3. 如果沒有讀鎖或寫鎖的話,如果需要阻塞或者CAS失敗,返回false;否則將當前執行緒置為獲得寫鎖的執行緒。

從上面可以看到呼叫了writerShouldBlock方法,FairSync的實現是如果等待佇列中有等待執行緒,則返回false,說明公平模式下,只要佇列中有執行緒在等待,那麼後來的這個執行緒也是需要記入佇列等待的;NonfairSync中的直接返回的直接是false,說明不需要阻塞。從上面的程式碼可以得出,當沒有鎖時,如果使用的非公平模式下的寫鎖的話,那麼返回false,直接通過CAS就可以獲得寫鎖。

2.2.2、讀鎖的獲取

讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問時,讀鎖總是能夠成功地被獲取到,而所做的也只是增加讀狀態。

如果當前執行緒已經獲取了讀鎖,則增加讀狀態。如果當前執行緒在獲取讀鎖時,寫鎖已經被其他執行緒獲取了,則進入等待狀態。

  • 第1步:呼叫呼叫內部類Re'a'dLock中的lock()方法
public static class ReadLock implements Lock, java.io.Serializable {
    // ...
            
    // 獲取讀鎖
    public void lock() {
        sync.acquireShared(1);  // 呼叫的是同步器AQS中的方法
    }
    // ...
}
  • 第2步:呼叫AQS中共享模式:acquireShared方法
if (tryAcquireShared(arg) < 0)
    doAcquireShared(arg);

當tryAcquireShared()方法小於0時,那麼會執行doAcquireShared方法將該執行緒加入到等待佇列中。 
Sync實現了tryAcquireShared方法,如下:

  • 第3步:呼叫Sync中的tryAcquireShared方法
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    
	// ...
	
    // 靜態內部類Sync,它的父類是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
        
        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);
            // 如果讀不應該阻塞並且讀鎖的個數小於最大值65535,並且可以成功更新狀態值,成功
            if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果當前讀鎖為0
                if (r == 0) {
                    // 第一個讀執行緒就是當前執行緒
                    firstReader = current;
                    firstReaderHoldCount = 1;
                }
                // 如果當前執行緒重入了,記錄firstReaderHoldCount
                else if (firstReader == current) {
                    firstReaderHoldCount++;
                }
                // 當前讀執行緒和第一個讀執行緒不同,記錄每一個執行緒讀的次數
                else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // 否則,迴圈嘗試
            return fullTryAcquireShared(current);
        }
        
        // ...
    }
}

從上面的程式碼以及註釋可以看到,分為三步: 

1. 如果當前有寫執行緒並且本執行緒不是寫執行緒,那麼失敗,返回 -1;

2. 否則,說明當前沒有寫執行緒或者本執行緒就是寫執行緒(可重入),接下來判斷是否應該讀執行緒阻塞並且讀鎖的個數是否小於最小值,並且CAS成功使讀鎖+1,成功,返回1。其餘的操作主要是用於計數的; 

3. 如果2中失敗了,失敗的原因有三,第一是應該讀執行緒應該阻塞;第二是因為讀鎖達到了上線;第三是因為CAS失敗,有其他執行緒在併發更新state,那麼會調動fullTryAcquireShared方法。

fullTryAcquiredShared方法如下:

final int fullTryAcquireShared(Thread current) {

    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        // 一旦有別的執行緒獲得了寫鎖,返回-1,失敗
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        } 
        // 如果讀執行緒需要阻塞
        else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            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;
        }
    }
}

從上面可以看到fullTryAcquireShared與tryAcquireShared有很多類似的地方。 在上面可以看到多次呼叫了readerShouldBlock方法,對於公平鎖,只要佇列中有執行緒在等待,那麼將會返回true,也就意味著讀執行緒需要阻塞;對於非公平鎖,如果當前有執行緒獲取了寫鎖,則返回true。一旦不阻塞,那麼讀執行緒將會有機會獲得讀鎖。

總結下獲取鎖:

1、如果當前沒有寫鎖或讀鎖時,第一個獲取鎖的執行緒都會成功,無論該鎖是寫鎖還是讀鎖;

2、如果當前已經有了讀鎖,那麼這時獲取寫鎖將失敗,獲取讀鎖有可能成功也有可能失敗;

3、如果當前已經有了寫鎖,那麼這時獲取讀鎖或寫鎖,如果執行緒相同(可重入),那麼成功;否則失敗。

2.3、鎖的釋放

獲取鎖要做的是更改AQS的狀態值以及將需要等待的執行緒放入到佇列中。釋放鎖要做的就是更改AQS的狀態值以及喚醒佇列中的等待執行緒來繼續獲取鎖。

2.3.1、寫鎖的釋放

  • 第1步:呼叫WriteLock類中的unlock方法
public void unlock() {
    sync.release(1);
}
  • 第2步:通過Sync呼叫AQS中的relaease方法
 public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • 第3步:呼叫Sync中的tryRelease方法

一旦釋放成功了,那麼如果等待佇列中有執行緒再等待,那麼呼叫unparkSuccessor將下一個執行緒解除掛起。 
Sync需要實現tryRelease方法。具體實現原始碼如下所示:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    
	// ...
	
    // 靜態內部類Sync,它的父類是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
        
        protected final boolean tryRelease(int releases) {
            // 如果沒有執行緒持有寫鎖,但是仍要釋放,丟擲異常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            // 如果沒有寫鎖了,那麼將AQS的執行緒置為null
            if (free)
                setExclusiveOwnerThread(null);
            // 更新狀態
            setState(nextc);
            return free;
        }
        
        // ...
    }
}

從上面可以看到,寫鎖的釋放主要有三步: 

1. 如果當前沒有執行緒持有寫鎖,但是還要釋放寫鎖,丟擲異常;

2. 得到解除一把寫鎖後的狀態,如果沒有寫鎖了,那麼將AQS的執行緒置為null;

3. 不管第二步中是否需要將AQS的執行緒置為null,AQS的狀態總是要更新的。

  • 從上面可以看到,返回true當且只當沒有寫鎖的情況下,還有寫鎖則返回false。

2.3.2、讀鎖的釋放

  • 第1步:呼叫ReadLock中的unlock方法
public void unlock() {
    sync.releaseShared(1);
}
  • 第2步:通過Sync呼叫AQS的releaseShared方法
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
  • 第3步:呼叫Sync的tryReleaseShared方法,如果釋放成功,呼叫doReleaseShared嘗試喚醒下一個節點
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    
	// ...
	
    // 靜態內部類Sync,它的父類是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
        
        protected final boolean tryReleaseShared(int unused) {
            // 得到呼叫unlock的執行緒
            Thread current = Thread.currentThread();
            // 如果是第一個獲得讀鎖的執行緒
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            }
            // 否則,是HoldCounter中計數-1
            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;
                // 如果CAS更新狀態成功,返回讀鎖是否等於0;失敗的話,則重試
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
        
        // ...
    }
}

從上面可以看到,釋放鎖的第一步是更新firstReader或HoldCounter的計數,接下來進入死迴圈,嘗試更新AQS的狀態,一旦更新成功,則返回;否則,則重試。 

釋放讀鎖對讀執行緒沒有影響,但是可能會使等待的寫執行緒解除掛起開始執行。所以,一旦沒有鎖了,就返回true,否則false;返回true後,那麼則需要釋放等待佇列中的執行緒,這時讀執行緒和寫執行緒都有可能再獲得鎖。

釋放鎖的總結:

1、如果當前是寫鎖被佔有了,只有當寫鎖的資料降為0時才認為釋放成功;否則失敗。因為只要有寫鎖,那麼除了佔有寫鎖的那個執行緒,其他執行緒即不可以獲得讀鎖,也不能獲得寫鎖; 

2、如果當前是讀鎖被佔有了,那麼只有在寫鎖的個數為0時才認為釋放成功。因為一旦有寫鎖,別的任何執行緒都不應該再獲得讀鎖了,除了獲得寫鎖的那個執行緒。


3、ReentrantReadWriteLock中的其他方法

3.1、getOwner()

getOwner方法用於返回當前獲得寫鎖的執行緒,如果沒有執行緒佔有寫鎖,那麼返回null。實現如下:

protected Thread getOwner() {
    return sync.getOwner();
}

可以看到直接呼叫了Sync的getOwner方法,下面是Sync的getOwner方法:

final Thread getOwner() {
    // Must read state before owner to ensure memory consistency
    return ((exclusiveCount(getState()) == 0) ? null : getExclusiveOwnerThread());
}

如果獨佔鎖的個數為0,說明沒有執行緒佔有寫鎖,那麼返回null;否則返回佔有寫鎖的執行緒。

3.2、getReadLockCount()

getReadLockCount()方法用於返回讀鎖的個數,實現如下:

public int getReadLockCount() {
    return sync.getReadLockCount();
}

可以看到呼叫了Sync中的getReadLockCount()方法:

final int getReadLockCount() {
    return sharedCount(getState());
}

static int sharedCount(int c) {
    return c >>> SHARED_SHIFT; 
}

從上面程式碼可以看出,要想得到讀鎖的個數,就是看AQS的state的高16位。這和前面講過的一樣,高16位表示讀鎖的個數,低16位表示寫鎖的個數。

3.3、getReadHoldCount()

getReadHoldCount()方法用於返回當前執行緒所持有的讀鎖的個數,如果當前執行緒沒有持有讀鎖,則返回0。

public int getReadHoldCount() {
    return sync.getReadHoldCount();
}

可以看到,呼叫了Sync中的getReadHoldCount()方法:

final int getReadHoldCount() {
    // 如果沒有讀鎖,自然每個執行緒都是返回0
    if (getReadLockCount() == 0)
        return 0;

    // 得到當前執行緒
    Thread current = Thread.currentThread();
    // 如果當前執行緒是第一個讀執行緒,返回firstReaderHoldCount引數
    if (firstReader == current)
        return firstReaderHoldCount;
    // 如果當前執行緒不是第一個讀執行緒,得到HoldCounter,返回其中的count
    HoldCounter rh = cachedHoldCounter;
    // 如果快取的HoldCounter不為null並且是當前執行緒的HoldCounter,直接返回count
    if (rh != null && rh.tid == getThreadId(current))
        return rh.count;

    // 如果快取的HoldCounter不是當前執行緒的HoldCounter,
    // 那麼從ThreadLocal中得到本執行緒的HoldCounter,返回計數         
    int count = readHolds.get().count;
    // 如果本執行緒持有的讀鎖為0,從ThreadLocal中移除
    if (count == 0) readHolds.remove();
    return count;
}

從上面的程式碼中,可以看到兩個熟悉的變數,firstReader和HoldCounter型別。這兩個變數在讀鎖的獲取中接觸過,前面沒有細說,這裡細說一下。HoldCounter類的實現如下:

static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

readHolds是ThreadLocalHoldCounter類,定義如下:

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
	public HoldCounter initialValue() {
	    return new HoldCounter();
	}
}

可以看到,readHolds儲存了每一個執行緒的HoldCounter,而HoldCounter中的count變數就是用來記錄執行緒獲得的寫鎖的個數。所以可以得出結論:Sync維持總的讀鎖的個數,在state的高16位;由於讀執行緒可以同時存在,所以每個執行緒還儲存了獲得的讀鎖的個數,這個是通過HoldCounter來儲存的。 

除此之外,對於第一個讀執行緒有特殊的處理,Sync中有如下兩個變數:

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader表示第一個得到讀鎖的執行緒,firstReaderHoldCount表示這個執行緒獲得的寫鎖。所以可以得出結論:第一個獲取到讀鎖的資訊儲存在firstReader中;其餘獲取到讀鎖的執行緒的資訊儲存在HoldCounter中。 

看完了HoldCounter和firstReader,再來看一下getReadLockCount的實現,主要有三步: 

1. 當前沒有讀鎖,那麼自然每一個執行緒獲得的讀鎖都是0; 

2. 如果當前執行緒是第一個獲取到讀鎖的執行緒,那麼返回firstReadHoldCount; 

3. 如果當前執行緒不是第一個獲取到讀鎖的執行緒,得到該執行緒的HoldCounter,然後返回其count欄位。如果count欄位為0,說明該執行緒沒有佔有讀鎖,那麼從readHolds中移除。獲取HoldCounter分為兩步,第一步是與cachedHoldCounter比較,如果不是,則從readHolds中獲取。

3.4、getWriteHoldCount()

getWriteHoldCount()方法返回當前執行緒所持有寫鎖的個數。

public int getWriteHoldCount() {
    return sync.getWriteHoldCount();
}

接著呼叫了Sync中的getWriteHoldCount()方法:

final int getWriteHoldCount() {
    return isHeldExclusively() ? exclusiveCount(getState()) : 0;
}

可以看到如果沒有執行緒持有寫鎖,那麼返回0;否則返回AQS的state的低16位。


4、總結

當分析ReentranctReadWriteLock時,或者說分析內部使用AQS實現的工具類時,需要明白的就是AQS的 state 代表的是什麼。ReentrantLockReadWriteLock中的 state 同時表示寫鎖和讀鎖的個數。為了實現這種功能,state的高16位表示讀鎖的個數,低16位表示寫鎖的個數。

AQS有兩種模式:共享模式和獨佔模式,讀寫鎖的實現中,讀鎖使用共享模式;寫鎖使用獨佔模式;

另外一點需要記住的即使,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個執行緒可以獲得讀鎖外,其他執行緒不能獲得讀鎖。


上一篇:重入鎖 --- ReentrantLock 詳解(點選檢視)