Java JUC ReentrantReadWriteLock解析

神祕傑克發表於2022-01-28

讀寫鎖 ReentrantReadWriteLock 原理

介紹

ReentrantReadWriteLock 和 ReentrantLock 的區別是,ReentrantLock 是獨佔鎖,同一時間只能有一個執行緒獲取鎖,但在實際中更多的是讀多寫少的情況,顯然 ReentrantLock 滿足不了該情況,而 ReentrantReadWriteLock 採用了讀寫分離的策略,可以允許多個執行緒同時進行讀取

類圖

從該類圖可以看到,在讀寫鎖內部維護了一個ReadLock和一個WriteLock,它們依賴Sync實現具體功能,而 Sync 繼承自 AQS,並且也提供了公平非公平的實現。

下面我們只介紹非公平的實現

我們知道在 AQS 中維護了一個 state 狀態,而在 ReentrantReadWriteLock 中則需要維護讀狀態和寫狀態,那麼一個 state 如何標識寫和讀兩種狀態呢?

ReentrantReadWriteLock 巧妙地使用 state 的高 16 位表示讀狀態,也就是獲取到讀鎖的次數;使用低 16 位表示獲取到寫鎖的執行緒的可重入次數

static final int SHARED_SHIFT   = 16;
//共享鎖(讀鎖)狀態單位值65536
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//共享鎖執行緒最大個數65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
//排它鎖(寫鎖)掩碼,二進位制,15個1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//返回讀鎖執行緒數量
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//返回寫鎖可重入可數
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

在 ReentrantReadWriteLock 類圖中,firstReader用來記錄第一個獲取到讀鎖的執行緒,firstReaderHoldCount則是記錄第一個獲取到讀鎖的執行緒可重入次數,cachedHoldCounter用來記錄最後一個獲取讀鎖的執行緒可重入次數。

readHolds是 ThreadLocal 變數,用來存放除第一個獲取讀執行緒外的其他執行緒獲取讀鎖的可重入次數。

ThreadLocalHoldCounter 繼承了 ThreadLocal,所以 initialValue 方法返回一個 HoldCounter 物件。

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

寫鎖的獲取與釋放

?:在 ReentrantReadWriteLock 中寫鎖是使用WriteLock類實現

void lock()

寫鎖為獨佔鎖,在某一時刻只能有一個執行緒獲取該鎖,如果當前沒有執行緒獲取到讀鎖和寫鎖,則當前執行緒可以獲取到寫鎖然後返回。如果當前已經有執行緒獲取到讀鎖和寫鎖,則當前請求寫鎖的執行緒會被阻塞掛起,寫鎖是可重入鎖,如果當前執行緒已經獲取了該鎖,則再次獲取時僅僅將可重入次數加 1 即可。

public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

可以看到在 lock 內部呼叫了 AQS 的 acquire 方法,其中 tryAcquire 則是在 ReentrantReadWriteLock 內部的 Sync 類重寫。如下:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
                      //(1) c != 0說明讀鎖或寫鎖已經被某執行緒獲取
            if (c != 0) {
                //(2) w == 0 說明已經有執行緒獲取到該鎖,w != 0並且當前執行緒並不是擁有者則返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //(3) 說明當前執行緒獲取到了寫鎖,判斷可重入次數
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //(4)可重入次數加一
                setState(c + acquires);
                return true;
            }
            //(5)第一個寫執行緒獲取寫鎖
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
}

在程式碼(1)中,如果當前 AQS 狀態值不為 0 則說明已經有執行緒獲取到了讀鎖或者寫鎖。

在程式碼(2)中,如果 w==0 則說明狀態值的低 16 位為 0,而 AQS 狀態值不為 0,暗示已經有執行緒獲取到了讀鎖,所以返回 false。而如果 w!=0 則說明當前已經有執行緒獲取到了寫鎖,那麼看看是不是自己獲得的,不是返回 false。

在程式碼(3)中,說明當前執行緒已經獲取到了該鎖,判斷該執行緒可重入次數是否大於最大值,是則丟擲異常,否則執行程式碼(4)進行增加可重入次數,然後返回 true。

如果 AQS 的狀態值等於 0 則說明目前沒有執行緒獲取到讀鎖和寫鎖,所以執行程式碼(5)。其中,對於 writerShouldBlock 方法,非公平鎖的實現為如下:

final boolean writerShouldBlock() {
      return false; // writers can always barge
}

可以看到該方法返回 false,說明需要進行 CAS 嘗試獲取寫鎖,獲取成功則設定當前鎖的持有人為當前執行緒,然後返回 true,否則返回 false。

公平鎖實現為如下:

final boolean writerShouldBlock() {
      return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

這裡還是使用 hasQueuedPredecessors 來判斷當前節點是否有前驅節點等,在《ReentrantLock 解析》文章已經講過了,這裡就不再多講了。

void lockInterruptibly()

類似於 lock 方法,它的不同之處在於,它會對中斷進行響應,也就是當其他執行緒呼叫了該執行緒的 interrupt 方法中斷了當前執行緒時,當前執行緒會丟擲異常 InterruptedException 異常。

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

boolean tryLock()

嘗試獲取寫鎖,如果當前沒有其他執行緒持有寫鎖或者讀鎖,則當前執行緒獲取寫鎖然後返回 true;如果已經有執行緒獲取寫鎖或讀鎖則返回 false,並且當前執行緒不會阻塞;如果當前執行緒已經持有了寫鎖則 state 值加 1 然後返回 true。實現與 tryAcquire 方法類似,不多講。

public boolean tryLock( ) {
    return sync.tryWriteLock();
}
final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

boolean tryLock(long timeout,TimeUnit unit)

與 tryAcquire 方法不同之處在於,多了超時時間引數,如果嘗試獲取寫鎖失敗後會把當前執行緒掛起指定時間,等待時間到後啟用該執行緒重試獲取,如果還是沒獲取到寫鎖則返回 false。另外,該方法會對中斷進行響應,也就是當其他執行緒呼叫了該執行緒的 interrupt 方法中斷了當前執行緒時,當前執行緒會丟擲 InterruptedException 異常。

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

void unlock()

嘗試釋放鎖,如果當前執行緒持有該鎖,呼叫後則會 AQS state 狀態值減 1,如果減 1 後為 0 則釋放該鎖,否則僅僅減 1。如果當前執行緒沒有持有該鎖而呼叫該方法則丟擲異常。

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    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();
   //獲取可重入值,這裡沒有考慮高16位,因為獲取寫鎖時狀態值肯定為0
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    //如果為0則釋放鎖,否則只是更新狀態值
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

讀鎖的獲取與釋放

?:在 ReentrantReadWriteLock 中讀鎖是使用ReadLock類實現

void lock()

獲取讀鎖,如果當前執行緒沒有持有該鎖,則當前執行緒獲取該鎖,AQS 狀態值 state 的高 16 位進行加 1,然後返回。否則如果其他執行緒持有寫鎖,則當前執行緒被阻塞。

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

在該段程式碼中,讀鎖的 lock 方法呼叫了 AQS 的 acquireShared 方法,隨後內部呼叫了 ReentrantReadWriteLock 中的 Sync 類重寫的 tryAcquireShared 方法。

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    //(1)獲取當前狀態值
    int c = getState();
    //(2)判斷是否被寫鎖佔用
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //(3)獲取讀鎖計數
    int r = sharedCount(c);
    //(4)嘗試獲取鎖,多個讀執行緒同時只有一個會成功,不成功的進入fullTryAcquireShared方法重試
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //(5)第一個執行緒獲取鎖,設定 firstReader 等於當前執行緒
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        //(6)如果當前執行緒是第一個獲取鎖的執行緒則進行第一個執行緒可重入次數加1
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            //(7)記錄最後一個獲取讀鎖的執行緒或其他執行緒讀鎖的可重入數
            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;
    }
      //(8)類似tryAcquireShared,但是是自旋獲取
    return fullTryAcquireShared(current);
}

該方法首先獲當前 AQS 狀態值,然後程式碼(2)檢視是否有其他執行緒獲取到了寫鎖,如果是則返回-1。然後放入 AQS 阻塞佇列。

如果當前要獲取讀鎖的執行緒已經持有了寫鎖,則也可以獲取讀鎖。但是需要注意,當一個執行緒先獲取了寫鎖,然後獲取了讀鎖處理事情完畢後,要記得把讀鎖和寫鎖都釋放掉,不能只釋放寫鎖。

如果執行到程式碼(3)首先獲取讀鎖的個數,到這裡說明目前沒有執行緒獲取到寫鎖,但是可能已經有執行緒獲取到了讀鎖,然後執行程式碼(4),其中非公平鎖的 readerShouldBlock 實現如下:

final boolean readerShouldBlock() {
      return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return //頭節點不允許為空,也就是阻塞佇列存在
            (h = head) != null &&
            //頭節點下一個節點必須存在
            (s = h.next)  != null &&
            //下一個節點不能共享,也就是寫鎖
            !s.isShared()         &&
            //下一個節點的執行緒物件不允許為空
        s.thread != null;
}

該方法作用是,如果阻塞佇列是空的,那麼可以獲取;如果阻塞佇列不是空的,分兩種情況。一:如果第一個節點是寫節點,那麼你不能獲取讀鎖,阻塞排隊。二,如果第一個節點是讀節點,那麼可以獲取。

隨後在程式碼(4)進行判斷當前執行緒當前獲取讀鎖的執行緒是否達到了最大值。最後執行 CAS 操作將 AQS 狀態值的高 16 位值增加 1。

程式碼(5)(6)記錄第一個獲取讀鎖的執行緒並統計該執行緒獲取讀鎖的可重入數。

程式碼(7)使用 cachedHoldCounter 記錄最後一個獲取到讀鎖的執行緒和該執行緒獲取讀鎖的可重入數,readHolds 記錄了當前執行緒獲取讀鎖的可重入數。

如果 readerShouldBlock 方法返回 true,則說明有執行緒正在獲取寫鎖,所以執行程式碼(8),fullTryAcquireShared 的程式碼與 tryAcquireShared 類似,它們的不同之處在於,前者通過迴圈自旋獲取。

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                //獲取狀態值
                int c = getState();
                //如果存在寫鎖
                if (exclusiveCount(c) != 0) {
                    //持有者不是自己 返回 -1
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                //如果寫鎖空閒,且可以獲取讀鎖
                } 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");
                // CAS 設定讀鎖,高位加1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    // sharedCount(c) == 0 說明讀鎖空閒 進行設定
                    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))
                            //建立一個cachedHoldCounter
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        //計數器自增
                        rh.count++;
                        // 更新快取計數器
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
}

void lockInterruptibly()

類似於 lock 方法,不同之處在於,該方法會對中斷進行響應,也就是當其他執行緒呼叫了該執行緒的 interrupt 方法中斷了當前執行緒時,當前執行緒會丟擲 InterruptedException 異常。

boolean tryLock()

嘗試獲取讀鎖,如果當前沒有其他執行緒持有寫鎖,則當前執行緒獲取讀鎖會成功,然後返回 true。如果當前已經有其他執行緒持有寫鎖則該方法直接返回 false,但當前執行緒並不會被阻塞。如果當前執行緒已經持有了該讀鎖則簡單增加 AQS 的狀態值高 16 位後直接返回 true。其程式碼類似 tryLock 的程式碼,這裡不再講述。

boolean tryLock(long timeout,TimeUnit unit)

與 tryLock 的不同之處在於,多了超時時間引數,如果嘗試獲取讀鎖失敗則會把當前執行緒掛起指定時間,待超時時間到後當前執行緒被啟用,如果此時還沒有獲取到讀鎖則返回 false。另外,該方法對中斷響應,也就是當其他執行緒呼叫了該執行緒的 interrupt 方法中斷了當前執行緒時,當前執行緒會丟擲 InterruptedException 異常。

void unlock()

讀鎖的釋放是委託給 Sync 來做,releaseShared 方法如下。

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果當前執行緒是第一個執行緒
    if (firstReader == current) {
        // 如果等於1 說明沒有重複獲取,則設定為null
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
          // 否則減1
            firstReaderHoldCount--;
    } else {//如果不是當前執行緒
        HoldCounter rh = cachedHoldCounter;
        // 如果快取是 null 或者快取所屬執行緒不是當前執行緒,則當前執行緒不是最後一個讀鎖
        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();
        // 減去一個讀鎖。對高16位減1
        int nextc = c - SHARED_UNIT;
        // 修改成功,如果是 0,表示讀鎖和寫鎖都空閒,則可以喚醒後面的等待執行緒
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

如上程式碼所示,在死迴圈中,首先獲取當前 AQS 狀態值並儲存到變數 c,然後 c 減去一個讀計數單位後使用 CAS 操作更新 AQS 狀態值,如果更新成果則檢視當前 AQS 狀態值是否為 0,為 0 說明沒有讀執行緒佔用讀鎖,則返回 true。

隨後呼叫 doReleaseShared 方法釋放一個由於獲取寫鎖而被阻塞的執行緒,如果當前 AQS 狀態值不為 0,則說明還有其他讀執行緒,所以返回 false。

鎖的升降級

升降級是指讀鎖升級為寫鎖,寫鎖降級為讀鎖。在 ReentrantReadWriteLock 讀寫鎖中,只支援寫鎖降級為讀鎖,而不支援讀鎖升級為寫鎖。

程式碼示例:

public class LockTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    public static void main(String[] args) {
        new Thread(() -> write()).start();
        new Thread(() -> read()).start();
    }

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "開始學習《Thinking in Java》");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "獲得到了寫鎖");
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "太難了!我不學了!");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "開始印刷《Thinking in Java》");
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "在寫鎖中獲取到了讀鎖");
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "印刷完成");
            writeLock.unlock();
        }
    }
}

執行結果:

Thread-0開始印刷《Thinking in Java》
Thread-0在寫鎖中獲取到了讀鎖
Thread-0印刷完成
Thread-1開始學習《Thinking in Java》

我們可以看到在寫鎖中成功獲得到了讀鎖,而在讀鎖中被一直阻塞。說明不支援鎖升級!

為什麼 ReentrantReadWriteLock 不支援鎖升級

主要是避免死鎖,例如兩個執行緒 A 和 B 都在讀, A 升級要求 B 釋放讀鎖,B 升級要求 A 釋放讀鎖,互相等待形成死迴圈。如果能嚴格保證每次都只有一個執行緒升級那也是可以的。

在 tryAcquireShared 方法和 fullTryAcquireShared 中都有體現,例如下面的判斷:

if (exclusiveCount(c) != 0) {
    if (getExclusiveOwnerThread() != current)
        return -1;

該程式碼意思是:當寫鎖被持有時,如果持有該鎖的執行緒不是當前執行緒,就返回獲取鎖失敗,反之就會繼續獲取讀鎖。稱之為鎖降級。

總結

  1. 讀寫鎖特點:讀鎖是共享鎖,寫鎖是排他鎖,讀鎖和寫鎖不能同時存在
  2. 插隊策略:為了防止執行緒飢餓,讀鎖不能插隊
  3. 升級策略:只能降級,不能升級
  4. ReentrantReadWriteLock 適合於讀多寫少的場合,可以提高併發效率,而 ReentrantLock 適合普通場合

總結

總結

ReentrantReadWriteLock 巧妙地使用 AQS 的狀態值的高 16 位表示獲取到讀鎖的個數,低 16 位表示獲取寫鎖的執行緒的可重入次數,並通過 CAS 對其進行操作實現了讀寫分離,非常適合在讀多寫少的場景下使用。

相關文章