Java併發之ReentrantReadWriteLock原始碼解析(二)

北洛發表於2021-07-08

先前,筆者和大家一起了解了ReentrantReadWriteLock的寫鎖實現,其實寫鎖本身實現的邏輯很少,基本上還是複用AQS內部的等待佇列思想。下面,我們來看看ReentrantReadWriteLock的讀鎖實現。

當呼叫讀鎖的lock()方法時,會呼叫到Sync的父類AQS實現的acquireShared(int arg)方法,在這個方法又會呼叫子類實現的tryAcquireShared(arg)方法嘗試獲取讀鎖,如果返回大於等於0,則代表獲取讀鎖成功,返回結果小於0代表獲取讀鎖失敗。則會執行doAcquireShared(arg)方法進入等待佇列。

下面我們來看看在tryAcquireShared(arg)方法中是如何嘗試獲取讀鎖的,tryAcquireShared(arg)方法首先會獲取讀寫鎖當前狀態c,如果exclusiveCount(c)的結果不為0則代表有執行緒佔有寫鎖,會接著判斷寫鎖的獨佔執行緒是否是當前請求讀鎖的執行緒,如果不是則進入<1>處的分支返回獲取讀鎖失敗。只有寫鎖不被其他執行緒持有,或者佔有寫鎖的執行緒請求讀鎖,才可以跳過分支<1>前進到程式碼<2>處。

在程式碼<2>處會獲取目前讀鎖被獲取的次數,之後會執行下readerShouldBlock()判斷當前請求讀鎖的執行緒是否應該阻塞,Sync並沒有實現這個方法,而是交由子類公平鎖和非公平鎖實現,這個方法的實現一般是判斷讀寫鎖的等待佇列中是否有執行緒,如果是公平鎖的實現,只要佇列中有等待讀寫鎖的執行緒,就會判定需要阻塞當前讀執行緒,如果是非公平鎖的實現,就會判斷當前佇列中最前面需要鎖的執行緒是讀執行緒還是寫執行緒,如果是讀執行緒則就不阻塞,大家可以一起共享讀鎖,如果是寫執行緒則需要阻塞。

之後會判斷獲取讀鎖的次數是否已到達MAX_COUNT,如果沒有到達才會執行CAS嘗試對獲取讀鎖的次數+1,由於獲取讀鎖的次數是儲存在state的前16位,所以這裡會加上SHARED_UNIT,並且這裡我們也看到tryAcquireShared(int unused)方法給傳入的獲取次數用的變數命名是unused,讀鎖在這裡並不使用外部傳入的獲取次數,因為這個獲取次數可能大於1,會出現當前獲取讀鎖的次數+1(SHARED_UNIT)剛好到MAX_COUNT,+2(2*SHARED_UNIT)超過MAX_COUNT。

如果判斷當前請求讀鎖的執行緒不阻塞,當前獲取讀鎖的次數小於MAX_COUNT且CAS對獲取讀鎖+1成功,則會進入<3>處的分支,如果原先讀鎖被獲取的次數為0,即r為0,代表當前執行緒是第一個獲取讀鎖的執行緒,之前說過第一個獲取讀鎖的執行緒會做一個優化,Sync的欄位firstReader用於指向第一個獲取讀鎖的執行緒,firstReaderHoldCount用於統計第一個執行緒獲取讀鎖的次數,因為可能出現執行緒在獲取讀鎖後又重新獲取的情況。當判斷執行緒是當前第一個獲取讀鎖的執行緒,會進入<4>處的分支,將firstReader指向當前執行緒,firstReaderHoldCount賦值為1,代表當前執行緒獲取一次讀鎖。如果原先讀鎖被獲取的次數不為0,且當前獲取讀鎖的執行緒為第一個獲取讀鎖的執行緒,則代表讀鎖被重入,這裡會進入<5>處的分支對firstReaderHoldCount+1。

如果<4>、<5>兩個條件都不滿足,原先讀鎖的獲取次數不為0,且當前獲取讀鎖的執行緒不是第一個獲取讀鎖的執行緒,則會進入<6>處的分支,這裡會先獲取cachedHoldCounter指向的HoldCounter物件,cachedHoldCounter會指向最後一個獲取讀鎖執行緒對應的HoldCounter物件(除了第一個獲取讀鎖的執行緒外),HoldCounter物件用於儲存不同執行緒獲取讀鎖的次數。如果rh為null,或者rh的執行緒id不是當前當前的執行緒id,這裡會進入<7>處,獲取執行緒區域性變數HoldCounter物件,並賦值給cachedHoldCounter。

之後判斷rh指向的HoldCounter物件獲取鎖的次數是否為0,如果為0會呼叫ThreadLocal.set(T value)儲存進執行緒的區域性變數,大家思考下為什麼這裡要呼叫ThreadLocal.set(T value)儲存區域性變數呢?如果當前讀鎖已經有執行緒在共享,當有新的執行緒獲取到讀鎖後會呼叫readHolds.get()方法執行ThreadLocalHoldCounter.initialValue()方法生成一個HoldCounter物件,雖然此時HoldCounter物件的獲取讀鎖次數count為0,但此時這個物件也快取線上程的區域性變數中了,為什麼這裡還要呼叫ThreadLocal.set(T value)儲存區域性變數呢?這是因為線上程釋放讀鎖的時候,如果判斷rh.count為0,會將執行緒對應的HoldCounter物件從執行緒區域性變數中移除。這裡可能出現cachedHoldCounter指向最後一個獲取讀鎖的執行緒的HoldCounter物件,執行緒釋放讀鎖後將HoldCounter物件從區域性變數中移除,但此時cachedHoldCounter依舊指向原先執行緒對應HoldCounter物件,並且執行緒在釋放讀鎖後又重新獲取讀鎖,且期間沒有其他執行緒獲取讀鎖,所以這裡判斷cachedHoldCounter指向的物件的執行緒id和當前執行緒id相同,就不會再呼叫readHolds.get()生成新的HoldCounter物件,而是複用舊的HoldCounter物件,如果HoldCounter為0,除了是新生成的,也有可能是上面所說的情況,這裡會重新儲存HoldCounter物件到執行緒的區域性變數中。之後對HoldCounter物件的count欄位+1表示獲取一次讀鎖,最後返回1表示獲取讀鎖成功。

上面的流程僅針對獲取讀鎖較為順利的情況,但在高併發場景下,很多事情都難以預料,比如在<3>處的readerShouldBlock() 方法返回執行緒應該阻塞,或者獲取讀鎖的次數已經到達MAX_COUNT,又或者執行CAS的時候失敗,有別的執行緒先當前執行緒獲取了讀鎖或者寫鎖,當前讀寫鎖的狀態已經和原始的狀態c不同了。只要出現這些情況都無法進入<3>處的分支。如果執行緒應該阻塞、讀鎖獲取次數到達上限,又或者寫鎖被佔有這些條件倒也罷了,如果僅僅是有執行緒先當前執行緒獲取了讀鎖改變了讀寫鎖狀態state,導致<3>處的CAS失敗從而無法獲取讀鎖,則顯得有些憋屈,因為讀鎖是允許共享的。因此,這裡還會在執行<9>處的fullTryAcquireShared(current)避免出現CAS未命中的情況。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
	//...	
    abstract static class Sync extends AbstractQueuedSynchronizer {
		//...
		abstract boolean readerShouldBlock();
		//...
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)//<1>
                return -1;
            int r = sharedCount(c);//<2>
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {//<3>
                if (r == 0) {//<4>
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {//<5>
                    firstReaderHoldCount++;
                } else {//<6>
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null ||
                        rh.tid != LockSupport.getThreadId(current))//<7>
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)//<8>
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);//<9>
        }
		//...
	}
	//...
    public static class ReadLock implements Lock, java.io.Serializable {
		//...
        public void lock() {
            sync.acquireShared(1);
        }
		//...
	}
	//...
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	//...
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
	//...
}

    

fullTryAcquireShared(Thread current)是對嘗試獲取讀鎖的再一重保障,實現思路其實也與上面的tryAcquireShared(int unused)很類似,只是多了一個輪詢直到判斷獲取讀鎖成功,或者讀寫鎖狀態不允許獲取讀鎖。

這裡依舊是在<1>處判斷當前是否有執行緒佔有寫鎖,如果寫鎖被佔有且獨佔寫鎖的執行緒不是當前請求讀鎖的執行緒則退出。再判斷寫鎖沒有被佔有的前提下,再判斷讀執行緒是否應該被阻塞,一般只有兩種情況會進入到分支<2>,當前有執行緒正在共享讀寫鎖的讀鎖,如果公平鎖發現目前等待佇列中有執行緒,這裡會判斷請求讀鎖的執行緒應該阻塞,如果是非公平鎖,則判斷等待佇列中是否有執行緒,如果有的話等待時長最久的執行緒是否請求寫鎖,如果是的話則要阻塞當前請求讀鎖的執行緒,如果不是當前請求讀鎖的執行緒可以和其他執行緒一起共享讀鎖。所以這個分支是針對目前已經有執行緒共享讀鎖,且等待佇列中有執行緒,又有新的執行緒來請求讀鎖做的判斷。如果出現這種情況則進入到分支<2>處,這裡會判斷下請求執行緒對應的HoldCounter物件的獲取讀鎖次數是否為0,正常情況應該為0,會把HoldCounter物件從區域性變數中移除,之後判斷獲取讀鎖次數為0,則返回-1表示獲取讀鎖失敗。正常情況下第一個獲取讀鎖的執行緒是不會進入到分支<2>,而除了第一個獲取讀鎖的執行緒外,其他已經獲取讀鎖的執行緒如果又重入讀鎖,是有會進入到分支<2>的,但這裡會判斷不是第一個執行緒,於是跳過分支<3>會進入分支<4>,又因為已經獲取到讀鎖只是重入讀鎖的執行緒對應的獲取讀鎖次數不為0,所以對應的HoldCounter物件不會被移除,也不會判斷獲取讀鎖次數為0而返回。

如果判斷當前執行緒不應阻塞,或者當前執行緒應當阻塞後又發現當前執行緒早已獲取到讀鎖,會繼而執行到<5>處的程式碼,判斷如果讀鎖被獲取的次數已達上限則報錯。如果未達上限,則會執行<6>處的CAS對讀鎖的獲取次數+1(SHARED_UNIT),如果CAS成功則會增加當前執行緒對讀鎖的獲取次數。如果<6>處的CAS失敗,可能有別的執行緒先當前執行緒修改了讀寫鎖的狀態,這裡會重新開始一輪新的迴圈,直到成功獲取到讀鎖,或者判斷有別的執行緒佔有了寫鎖。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
	//...	
    abstract static class Sync extends AbstractQueuedSynchronizer {
		//...
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {//<1>
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {//<2>
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {//<3>
                        // assert firstReaderHoldCount > 0;
                    } else {//<4>
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null ||
                                rh.tid != LockSupport.getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)//<5>
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {//<6>
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null ||
                            rh.tid != LockSupport.getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }
		//...
	}
	//...
}

  

下面我們再來看看公平鎖和非公平鎖是如何判斷是否應該阻塞當前請求讀鎖的執行緒, 首先可以看到公平鎖的實現非常簡單,就僅僅是判斷佇列中是否有等待執行緒,哪怕這些執行緒都是讀執行緒,可以一起共享讀鎖,公平鎖也會要求當前請求讀鎖的執行緒先入隊等待,直到它前一個執行緒獲取讀鎖後喚醒自己。而非公平鎖則是先判斷佇列中頭節點的後繼節點是否為null,如果非空再判斷是否是讀鎖,如果是寫鎖那當前請求讀鎖的執行緒只能先乖乖入隊,如果當前執行緒和頭節點的後繼節點同為讀執行緒,就判斷不阻塞,當前執行緒可以嘗試獲取讀鎖。非公平鎖相較公平鎖,多了一種高併發下佇列中的執行緒被無限期推遲的可能,如果頭節點的後繼節點是寫執行緒倒也好說,讀執行緒只能乖乖入隊,不會延期寫執行緒獲取寫鎖,但如果後繼節點為讀執行緒,且不斷有新的讀執行緒成功獲取讀鎖,那麼後繼節點的讀執行緒將被延期,因為每次嘗試用CAS修改讀寫鎖的狀態都會失敗,這裡的延期也包括後繼節點之後的所有節點,不管是共享節點還是獨佔節點。

static final class FairSync extends Sync {
	//...
	final boolean readerShouldBlock() {
		return hasQueuedPredecessors();
	}
}

static final class NonfairSync extends Sync {
	//...
	final boolean readerShouldBlock() {
		/* As a heuristic to avoid indefinite writer starvation,
		 * block if the thread that momentarily appears to be head
		 * of queue, if one exists, is a waiting writer.  This is
		 * only a probabilistic effect since a new reader will not
		 * block if there is a waiting writer behind other enabled
		 * readers that have not yet drained from the queue.
		 */
		return apparentlyFirstQueuedIsExclusive();
	}
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	//...
	final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
	//...
}

  

在瞭解讀鎖是如何加鎖後,我們來看看如何釋放讀鎖。當呼叫讀鎖(ReadLock)的unlock()方法時,會呼叫Sync的父類AQS實現的releaseShared(int arg)方法,在這個方法又會先呼叫子類實現的tryReleaseShared(arg)釋放讀鎖,如果釋放成功後,再呼叫doReleaseShared()嘗試喚醒後繼節點。下面我們就來了解下Sync類實現的tryReleaseShared(int unused),看看這個方法是如何釋放讀鎖的。

在Sync的tryReleaseShared(int unused)方法中,會先判斷當前釋放讀鎖的執行緒是否是第一個獲取讀鎖的執行緒,如果是的話則進入<1>處的分支,判斷第一個執行緒獲取讀鎖的次數,如果大於1,代表第一個執行緒在第一次獲取讀鎖後又重入讀鎖,這裡簡單地對獲取讀鎖的次數-1,如果判斷釋放的時候獲取次數是1,則代表第一個執行緒將完全釋放讀鎖,這裡會置空firstReader的指向,但不會將firstReaderHoldCount清零,因為當所有讀執行緒都完全釋放讀鎖後,如果再有新的讀執行緒請求讀鎖,會重新對firstReader、firstReaderHoldCount賦值。

如果釋放讀鎖的執行緒不是第一個執行緒,會先獲取cachedHoldCounter物件,即上一個獲取讀鎖執行緒對應的HoldCounter物件,判斷HoldCounter物件對應的執行緒是否是當前執行緒,如果是的話則不需要去執行緒區域性變數查詢與之對應的HoldCounter,如果不是則需要查詢,在確定好執行緒對應的HoldCounter物件後,如果判斷執行緒對鎖的獲取次數是1,則代表當前執行緒將完全釋放讀鎖,這裡會將HoldCounter物件從區域性變數移除,再判斷HoldCounter物件的獲取次數是否為0,如果為0則代表當前執行緒沒有先獲取讀鎖就先釋放讀鎖,這裡會丟擲unmatchedUnlockException異常。之後會對執行緒獲取鎖的次數-1。

最後會用CAS的方式對讀寫鎖的讀鎖被獲取數量-1(SHARED_UNIT),這裡可能存在讀執行緒併發釋放讀鎖的情況,所以這裡可能存在CAS失敗的情況,如果這裡失敗則會一直輪詢直到CAS成功,如果CAS成功則判斷當前狀態是否有執行緒佔有讀鎖或者寫鎖,如果CAS成功後讀寫鎖的狀態為0,代表當前無讀鎖也無寫鎖,則會返回讀鎖被完全釋放。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
	//...
    abstract static class Sync extends AbstractQueuedSynchronizer {
		//...
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {//<1>
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {//<2>
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {//<3>
                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;
            }
        }
		//...
	}
	//...
	public static class ReadLock implements Lock, java.io.Serializable {
		//...
        public void unlock() {
            sync.releaseShared(1);
        }
		//...
	}
	//...
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	//...
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
	//...
}

  

讀鎖的tryReadLock()方法只以非公平的方式獲取讀寫鎖,不管其本身實現是公平鎖還是非公平鎖,這裡會直接呼叫Sync實現的tryReadLock()方法,在這個方法中會先判斷當前是否有執行緒佔有寫鎖,如果有執行緒佔有寫鎖,且當前請求讀鎖的執行緒和佔有寫鎖的執行緒不是同一個,這裡會返回獲取讀寫鎖失敗。否則會判斷當前讀鎖被獲取次數是否已達上限MAX_COUNT,達到上限則報錯。如果讀鎖被獲取次數未達上限,才可以用CAS的方式對讀鎖的獲取次數+1(SHARED_UNIT),如果CAS失敗,代表當前有其他執行緒一起獲取讀鎖,狀態c已經被改變,會重新開始新的一輪嘗試獲取讀鎖流程。如果CAS成功則會增加執行緒對讀鎖的引用次數,之所以有這個HoldCounter物件,或者用firstReaderHoldCount欄位統計第一個執行緒引用所的次數,主要是為了確保線上程執行釋放讀鎖的時候,執行緒一定是之前獲取過讀鎖的執行緒。如果讀鎖不能保證釋放讀鎖的執行緒一定是之前獲取過讀鎖的執行緒,則會出現執行緒A獲取了讀鎖但尚未釋放,此時執行緒B未獲取讀鎖但直接釋放讀鎖,讀寫鎖狀態回到0,可由讀執行緒或者寫執行緒獲取,執行緒C獲取寫鎖,那就出現了本章最開始講的,一個執行緒在訪問資源,另一個執行緒在修改資源,這是非常危險的。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
	//...
    abstract static class Sync extends AbstractQueuedSynchronizer {
		//...
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null ||
                            rh.tid != LockSupport.getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }
		//...
	}
	//...
	public static class ReadLock implements Lock, java.io.Serializable {
		//...
	    public boolean tryLock() {
            return sync.tryReadLock();
        }
	}
	//...
}

  

最後,我們簡單看下讀鎖的tryLock(long timeout, TimeUnit unit)方法,這個方法會呼叫到AQS實現的tryAcquireSharedNanos(int arg, long nanosTimeout),相信大家看到這個方法的實現不會陌生,首先會呼叫子類實現的tryAcquireShared(arg)嘗試獲取讀鎖,如果獲取失敗,則會呼叫doAcquireSharedNanos(arg, nanosTimeout)嘗試將當前請求讀鎖的執行緒掛起。這兩個方法先前已經講過,這裡就不再贅述。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
	//...
	public static class ReadLock implements Lock, java.io.Serializable {
		//...
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }
		//...
	}
	//...
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	//...
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }
	//...
}

  

至此,讀寫鎖的原始碼解析到此結束,這裡很多AQS的方法之前筆者在ReentrantLockSemaphore的原始碼解析已經講過,請在看此章之前一定一定一定要先去看或者兩個章節的原始碼解析。如果有徹底理解這兩個章節,大家就會知道其實不管是可重入互斥鎖、訊號量、可重入讀寫鎖本身實現的業務並不多,它們的核心思想就是把執行緒視為一個個可入隊的節點,只是這些節點有的會獨佔互斥鎖或者寫鎖,有的可以和別的節點一起共享一個鎖,通過用不同的角度看待AQS,可以實現適用於不同場景下的併發類。

相關文章