動態

詳情 返回 返回

源碼解讀 | Java中ReentrantReadWriteLock的實現原理 - 動態 詳情

本文將介紹Java中ReentrantReadWriteLock的實現原理,從JDK源碼層面講解讀寫鎖的加鎖、釋放鎖的流程,最後對流程進行總結。

讀寫鎖概述

讀寫鎖 ReentrantReadWriteLock 的依賴關係如下圖所示。

讀寫鎖的基本使用如下

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

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

ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
writeLock.unlock();

讀鎖和寫鎖使用同一個 Sync 同步器,即使用同一個等待隊列和 state。讀鎖狀態使用 state 高 16 位存儲,寫鎖狀態使用 state 低 16 位存儲。

讀鎖涉及兩個重入計數:state(高 16 位)用於記錄有多少個線程持有該讀鎖,HoldCounter 用於記錄當前線程重入該讀鎖多少次,每個線程均有一個對應的 HoldCounter 對象。

寫鎖涉及一個重入計數:state(低 16 位)用於記錄有多少個線程持有該寫鎖。

讀鎖不支持條件變量,寫鎖支持條件變量。

寫鎖加鎖流程

WriteLock 的 lock() 方法會調用同步器的 acquire()方法。

// WriteLock implements Lock,
public void lock() {
    sync.acquire(1);
}

同步器的 acquire() 方法會先調用 tryAcquire() 方法嘗試獲取寫鎖,獲取寫鎖失敗則調用 AbstractQueuedSynchronizer 的全參 acquire() 方法將線程加入等待隊列。

// AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg))
        // 獲取寫鎖失敗則將線程加入等待隊列
        acquire(null, arg, false, false, false, 0L);
}

在介紹 ReentrantLock 的實現原理時,已對 AbstractQueuedSynchronizer 的全參 acquire() 方法進行了較為詳細的介紹,可參照 源碼解讀 | Java中ReentrantLock的實現原理,此處不再贅述,重點來看一下 tryAcquire() 方法如何獲取寫鎖。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 寫鎖狀態
    int w = exclusiveCount(c);
    // 已加寫鎖或者讀鎖
    if (c != 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;
    }
    // 未加鎖
    if (
        // 寫鎖應該等待
        writerShouldBlock() ||
        // 嘗試獲取寫鎖失敗
        !compareAndSetState(c, c + acquires)
    )
        // 加鎖失敗
        return false;
    // 加鎖成功
    // 將鎖的持有者置為當前線程
    setExclusiveOwnerThread(current);
    return true;
}

NonfairSync 和 FairSync 對 writerShouldBlock() 有不同的實現。對於非公平鎖,當前寫線程可直接參與競爭,不應該等待,競爭失敗才加入等待隊列。對於公平鎖,當前寫線程不應該直接參與競爭,應該等待,加入等待隊列。

// NonfairSync extends Sync
final boolean writerShouldBlock() {
    // 直接參與競爭,不等待
    return false;
}
// FairSync extends Sync
final boolean writerShouldBlock() {
    // 等待隊列中是否存在未取消的等待線程
    // 存在未取消的線程則需要等待
    return hasQueuedPredecessors();
}

讀鎖加鎖流程

ReadLock 的 lock() 方法會調用同步器的 acquireShared() 方法。

// ReadLock implements Lock
public void lock() {
    sync.acquireShared(1);
}

同步器的 acquireShared() 方法會先調用 tryAcquireShared() 方法嘗試獲取讀鎖,獲取讀鎖失敗則調用 AbstractQueuedSynchronizer 全參的 acquire() 方法將線程加入等待隊列。

// AbstractQueuedSynchronizer
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        // 獲取讀鎖失敗則將線程加入等待隊列
        acquire(null, arg, true, false, false, 0L);
}

在介紹 ReentrantLock 的實現原理時,已對 AbstractQueuedSynchronizer 的全參 acquire() 方法進行了較為詳細的介紹,可參照 [ReentrantLock 原理](),此處不再贅述,重點來看一下 tryAcquireShared() 方法如何獲取寫鎖。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
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);
    if (
        // 讀鎖不應該等待
        !readerShouldBlock() &&
        // 未越界
        r < MAX_COUNT &&
        // 嘗試獲取讀鎖成功
        compareAndSetState(c, c + SHARED_UNIT)
    ) {
        // 以下三個if-else分支用於讀鎖重入計數,忽略不影響理解主要加鎖解鎖流程
        // 未加讀鎖
        if (r == 0) {
            // 第一個獲取讀鎖的線程
            firstReader = current;
            firstReaderHoldCount = 1;
        } 
        // 第一個獲取讀鎖的線程重入
        else if (firstReader == current) {
            firstReaderHoldCount++;
        } 
        // 已加讀鎖且當前線程非讀重入
        else {
            // cachedHoldCounter緩存最近(上一個)獲取讀鎖的線程的計數器
            // rh:recent HoldCounter
            HoldCounter rh = cachedHoldCounter;
            if (
                // 上一個獲取讀鎖的線程為null
                rh == null ||
                // 上一個獲取讀鎖的線程不是當前線程
                rh.tid != LockSupport.getThreadId(current)
               )
                // 獲取當前線程的計數器並緩存
                cachedHoldCounter = rh = readHolds.get();
            else if (
                // 上一個獲取讀鎖的線程為當前線程且計數為0(已被移除)
                rh.count == 0
            )
                // 重新將當前線程放入哈希表記錄
                readHolds.set(rh);
            // 更新計數(計數加1)
            rh.count++;
        }
        return 1;
    }
    // 讀線程應該等待或者嘗試獲取鎖失敗或者越界
    // 再嘗試獲取讀鎖
    return fullTryAcquireShared(current);
}

NonfairSync 和 FairSync 對 readerShouldBlock() 有不同的實現。對於非公平鎖,只要等待隊列第一個等待線程不是寫線程,那麼當前讀線程就可直接參與競爭,不應該等待,競爭失敗才加入等待隊列。對於公平鎖,當前讀線程不應該直接參與競爭,應該等待,加入等待隊列。

// NonfairSync extends Sync
final boolean readerShouldBlock() {
    // 等待隊列第一個等待線程是否為寫線程
    // 若為寫線程,則讀線程需要等待
    // 不直接返回false是為避免寫線程無限等待
    return apparentlyFirstQueuedIsExclusive();
}
// FairSync extends Sync
final boolean readerShouldBlock() {
    // 等待隊列中是否存在未取消的等待線程
    // 存在未取消的線程則需要等待
    return hasQueuedPredecessors();
}

在 AbstractQueuedSynchronizer 的 全參acquire() 方法中,如果喚醒的是讀線程,會調用 tryAcquireShared() 方法嘗試獲取鎖,獲取成功後,如果下一個等待線程是讀線程,則會喚醒下一個等待線程。

// 如果讀線程被喚醒,則調用 tryAcquireShared() 方法嘗試獲取鎖
if (shared)
    acquired = (tryAcquireShared(arg) >= 0);
// 如果讀線程被喚醒並獲取鎖成功,並且下一個等待線程仍為讀線程,則由當前讀線程直接去喚醒下一個等待線程
if (acquired) {
    ...
    if (shared)
        signalNextIfShared(node);
    ...
}
// AbstractQueuedSynchronizer
private static void signalNextIfShared(Node h) {
    Node s;
    // 如果下一個等待線程為讀線程,則喚醒下一個線程
    if (h != null && (s = h.next) != null &&
        (s instanceof SharedNode) && s.status != 0) {
        s.getAndUnsetStatus(WAITING);
        LockSupport.unpark(s.waiter);
    }
}

寫鎖釋放鎖流程

WriteLock 的 unlock() 方法會調用同步器的 release()方法

// WriteLock
public void unlock() {
    sync.release(1);
}

同步器的 release() 方法會先調用 tryRelease() 方法嘗試釋放寫鎖,釋放成功後將喚醒下一個線程。

// AbstractQueuedSynchronizer
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        signalNext(head);
        return true;
    }
    return false;
}

公平鎖和非公平鎖釋放寫鎖的流程相同。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    // 寫鎖的持有者才能釋放寫鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 更新重入次數
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    // 重入次數為0則將寫鎖的持有者置為null
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    // 重入次數為0時認為寫鎖釋放成功
    return free;
}

讀鎖釋放流程

ReadLock 的 unlock() 方法會調用 同步器的 releaseShared() 方法。

// ReadLock implements Lock
public void unlock() {
    sync.releaseShared(1);
}

同步器的 releaseShared() 方法會先調用 tryReleaseShared() 方法嘗試釋放鎖,釋放成功後則會喚醒下一個等待線程。

// AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
    // 嘗試釋放讀鎖
    if (tryReleaseShared(arg)) {
        // 喚醒下一等待線程
        signalNext(head);
        return true;
    }
    return false;
}

公平鎖和非公平鎖釋放讀鎖的流程相同。注意到,釋放讀鎖時使用循環反覆嘗試,因為讀鎖是共享的,可被多個線程持有。

// Sync extends AbstractQueuedSynchronizer
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // ...修改線程重入計數...
    // 嘗試釋放鎖
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 重入次數為0時認為讀鎖釋放成功
            return nextc == 0;
    }
}

讀寫鎖流程總結

寫鎖加鎖流程

獲取寫鎖失敗的情況

  • 已加讀鎖,讀寫互斥;
  • 已加寫鎖且非重入,寫寫互斥;
  • 公平鎖:等待隊列存在未取消的線程。

獲取寫鎖前必須釋放讀鎖:

若線程 t 加讀鎖後,在釋放讀鎖前,又加寫鎖,由於已加讀鎖,寫鎖獲取失敗,線程 t 陷入等待,又導致讀鎖無法釋放,從而產生死鎖。

讀鎖加鎖流程

獲取讀鎖失敗的情況

  • 已加寫鎖非重入,寫讀互斥;
  • 非公平鎖:等待隊列中第一個等待線程為寫線程;
  • 公平鎖:等待隊列存在未取消的線程。

讀線程被喚醒後,如果等待隊列下一個等待線程為讀線程,則會直接將其喚醒。

寫鎖釋放流程

嘗試釋放寫鎖,釋放成功後將下一個等待線程喚醒。

如果線程不是寫鎖的持有者而去嘗試釋放寫鎖,則會拋異常 IllegalMonitorStateException。

// Sync extends AbstractQueuedSynchronizer 
// tryRelease()
//
if (!isHeldExclusively())
    throw new IllegalMonitorStateException();

僅當寫鎖重入計數為 0 時,才認為寫鎖釋放成功。

讀鎖釋放流程

嘗試釋放讀鎖,釋放成功後將下一個等待線程喚醒。

如果線程不是讀鎖的持有者而去嘗試釋放讀鎖,則會拋異常 unmatchedUnlockException。

// Sync extends AbstractQueuedSynchronizer 
// tryReleaseShared()
if (count <= 0)
    // 重入計數小於等於0説明當前線程未持有讀鎖
    throw unmatchedUnlockException();

僅當讀鎖重入計數為 0 時,才認為讀鎖釋放成功。

END

如果覺得本文對您有一點點幫助,歡迎點贊、轉發加關注,這會對我有非常大的幫助,如果有任何問題,歡迎在評論區留言或者後台私信,咱們下期見!

文章文檔:公眾號 字節幺零二四 回覆關鍵字可獲取本文文檔。

user avatar y_luoe_hai_61a734cbf3c94 頭像 u_11365552 頭像 chaoshenjinghyperai 頭像 leguandeludeng 頭像 tecdat 頭像 houbinbin 頭像 gvison 頭像 flydean 頭像 chengxy 頭像 dengjijie 頭像 itxiaoma 頭像 heerduo 頭像
點贊 38 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.