Java併發——讀寫鎖ReentrantReadWriteLock

午夜12點發表於2018-08-01

簡介

ReentrantReadWriteLock即可重入讀寫鎖,同樣也依賴於AQS來實現。在介紹ReentrantLock我們知道其依託AQS的同步狀態來判斷鎖是否佔有,而ReentrantReadWriteLock既有讀鎖又有寫鎖,是如何依靠一個狀態來維持的?

ReentrantReadWriteLock

ReentrantReadWriteLock讀寫鎖,與ReentrantLock一樣預設非公平,內部定義了讀鎖ReadLock()和寫鎖WriteLock(),在同一時間允許被多個讀執行緒訪問,但在寫執行緒訪問時,所有讀執行緒和寫執行緒都會被阻塞。讀寫鎖主要特性:公平性、可重入性、鎖降級

寫鎖的獲取與釋放

  • 寫鎖獲取
  • 寫鎖是一個支援重進入的排它鎖,其獲取的核心方法:

    
            protected final boolean tryAcquire(int acquires) {
                // 獲取當前執行緒
                Thread current = Thread.currentThread();
                // 獲取ReentrantReadWriteLock鎖整體同步狀態
                int c = getState();
                // 獲取寫鎖同步狀態
                int w = exclusiveCount(c);
                // 存在讀鎖或寫鎖
                if (c != 0) {
                    // c != 0 && w == 0 即若存在讀鎖或寫鎖持有執行緒不是當前執行緒,獲取寫鎖失敗
                    if (w == 0 || current != getExclusiveOwnerThread())
                        return false;
                    // 最多65535次重入,若超過報錯
                    if (w + exclusiveCount(acquires) > MAX_COUNT)
                        throw new Error("Maximum lock count exceeded");
                    // 可重入,設定同步狀態
                    setState(c + acquires);
                    return true;
                }
                // 公平與非公平,同步佇列是否有節點,同時cas設定狀態
                if (writerShouldBlock() ||
                    !compareAndSetState(c, c + acquires))
                    return false;
                // 設定獲取鎖的執行緒為當前執行緒
                setExclusiveOwnerThread(current);
                return true;
            }
    複製程式碼

    從原始碼中我們可以發現getState()獲取的是讀鎖與寫鎖總同步狀態,再通過exclusiveCount()方法單獨獲取寫鎖同步狀態

    
        static final int SHARED_SHIFT   = 16;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
            
        static int exclusiveCount(int c) {
            return c & EXCLUSIVE_MASK; 
        }
    複製程式碼

    ReentrantReadWriteLock通過按位切割state變數,同步狀態的低16位表示寫鎖獲取次數,高16位表示讀鎖獲取次數,如圖示意

    Java併發——讀寫鎖ReentrantReadWriteLock

    所以這解釋了為什麼寫鎖獲取次數最多65535次

    寫鎖獲取整體思路:當讀鎖已經被讀執行緒獲取或者寫鎖已經被其他寫執行緒獲取,則寫鎖獲取失敗;否則,獲取成功並可重入,增加寫鎖同步狀態

  • 寫鎖釋放
  • 
            protected final boolean tryRelease(int releases) {
                // 若釋放的執行緒不為鎖的持有者
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                // 重新設定同步狀態
                int nextc = getState() - releases;
                // 若新的寫鎖持有執行緒數為0,則將鎖的持有執行緒置為null
                boolean free = exclusiveCount(nextc) == 0;
                if (free)
                    setExclusiveOwnerThread(null);
                // 更新同步狀態    
                setState(nextc);
                return free;
            }
    複製程式碼
    寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0 時表示寫鎖已被釋放,從而等待的讀寫執行緒能夠繼續訪問讀寫鎖,同時前次寫執行緒的修改對後續讀寫執行緒可見

    讀鎖的獲取與釋放

    讀鎖相對於寫鎖(獨佔鎖或排他鎖),讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(執行緒安全的)增加讀狀態

  • 讀鎖的獲取
  • 
            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);
                // 判斷讀鎖是否需要公平,讀鎖持有執行緒數是否小於極值,CAS設定讀鎖狀態
                if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                    // 若讀鎖未被執行緒佔有,則更新firstReader和firstReaderHoldCount
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    // 如果獲取讀鎖的執行緒為第一次獲取讀鎖的執行緒,則firstReaderHoldCount重入數 + 1    
                    } 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);
            }
    複製程式碼

    讀鎖獲取整體思路
    ①.判斷寫鎖是否被佔有,寫鎖佔有執行緒是否不是當前執行緒,若成立則讀鎖獲取失敗
    ②.判斷讀鎖是否需要公平,讀鎖持有執行緒數是否小於極值,CAS設定讀鎖狀態成功,若條件不滿足,會呼叫fullTryAcquireShared()方法自旋再次嘗試獲取讀鎖;若條件滿足修改當前執行緒HoldCounter的值
    
            final int fullTryAcquireShared(Thread current) {
                HoldCounter rh = null;
                for (;;) {
                    int c = getState();
                    // 若寫鎖已被佔有,且寫鎖佔有執行緒不是當前執行緒
                    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");
                    // cas設定成功    
                    if (compareAndSetState(c, c + SHARED_UNIT)) {
                        // 若讀鎖未被執行緒佔有,則更新firstReader和firstReaderHoldCount
                        if (sharedCount(c) == 0) {
                            firstReader = current;
                            firstReaderHoldCount = 1;
                        // 如果獲取讀鎖的執行緒為第一次獲取讀鎖的執行緒,則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;
                    }
                }
            }
    複製程式碼
  • 讀鎖的釋放
  • 
            protected final boolean tryReleaseShared(int unused) {
                Thread current = Thread.currentThread();
                // 若當前執行緒為第一個獲取讀鎖的執行緒
                if (firstReader == current) {
                    // 若只有獲取一次,將firstReader置為null
                    if (firstReaderHoldCount == 1)
                        firstReader = null;
                    // 若多次,firstReaderHoldCount-1
                    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;
                }
                // 自旋CAS更新讀鎖同步狀態
                for (;;) {
                    int c = getState();
                    int nextc = c - SHARED_UNIT;
                    if (compareAndSetState(c, nextc))
                        return nextc == 0;
                }
            }
    複製程式碼
  • HoldCounter
  • HoldCounter在讀鎖中起到了很重要的作用,用來計算每個執行緒的讀鎖重入次數,並使用ThreadLocal型別的HoldCounter,可以記錄每個執行緒的鎖的重入次數。 cachedHoldCounter記錄了最後1個獲取讀鎖的執行緒的重入次數。 firstReader指向了第一個獲取讀鎖的執行緒,firstReaderHoldCounter記錄了第一個獲取讀鎖的執行緒的重入次數
    
        static final class HoldCounter {
            int count = 0;
            final long tid = getThreadId(Thread.currentThread());
        }
    
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
        
        private transient HoldCounter cachedHoldCounter;
        private transient int firstReaderHoldCount;
        private transient Thread firstReader = null;
    複製程式碼
    複製程式碼

    複製程式碼

    鎖降級

    鎖降級指的是寫鎖降級成為讀鎖,即先獲取寫鎖、獲取讀鎖在釋放寫鎖的過程,目的為了保證資料的可見性。假設有兩個執行緒A、B,若執行緒A獲取到寫鎖,不獲取讀鎖而是直接釋放寫鎖,這時執行緒B獲取了寫鎖並修改了資料,那麼執行緒A無法知道執行緒B的資料更新。如果執行緒A獲取讀鎖,即遵循鎖降級的步驟,則執行緒B將會被阻塞,直到執行緒A使用資料並釋放讀鎖之後,執行緒B才能獲取寫鎖進行資料更新。

    總結

    當有執行緒獲取讀鎖時,不允許再有執行緒獲得寫鎖
    當有執行緒獲得寫鎖時,不允許其他執行緒獲得讀鎖和寫鎖
    寫鎖能降級為讀鎖,讀鎖無法升級成寫鎖

    感謝

    《Java併發程式設計的藝術》
    http://cmsblogs.com/?p=2213

    相關文章