ReentrantReadWriteLock

ReentrantLock是獨占鎖,只允許一個線程執行;CountDownLatch,Semaphore等是共享鎖;它們分別利用了AQS的獨占與共享功能;那么如果在讀操作遠多于寫操作的情況下該如何選擇?讀寫鎖,之前的文章中介紹了如何自己實現一個讀寫鎖,還實現了重入功能,讀讀,寫寫,讀寫,寫讀四種重入。現在來看看JUC包下的ReentrantReadWriteLock的實現。
先來大致了解下ReentrantReadWriteLock:

  • 讀鎖是個共享鎖,寫鎖是個獨占鎖。讀鎖同時被多個線程獲取,寫鎖只能被一個線程獲取。讀鎖與寫鎖不能同時存在。
  • 一個線程可以多次重復獲取讀鎖和寫鎖
  • 鎖降級:獲取寫鎖的線程又獲取了讀鎖,之后釋放寫鎖,就完成了一次鎖降級。
  • 鎖升級:不支持升級。獲取讀鎖的線程去獲取寫鎖的化會造成死鎖。
  • 重入數:讀鎖和寫鎖的最大重入數為65535
  • 公平與非公平兩種模式

AQS維護了一個int值,表示同步狀態;對于ReentrantLock,state會在0與1之間變化,1表示已被占有后續線程入隊列等待,0表示free。對于CountDownLatch,會先將state賦予個大于0的值,在該值變為0后喚醒等待隊列中的線程。那么如何用它來即表示讀鎖又表示寫鎖呢?讀鎖我們是允許多個線程同步運行的,我們還允許重入,那么拿什么來記錄每個線程讀鎖的重入數?

針對上面兩個問題,對于同步狀態status,高16位表示所有線程持有的讀鎖總數,低16位為一個線程的寫鎖總數,包括重入。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 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; }

采用ThreadLocal來記錄每個線程鎖持有的讀鎖數目。

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

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

        private transient ThreadLocalHoldCounter readHolds;
        private transient HoldCounter cachedHoldCounter;
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;
  • HoldCounter 靜態內部類用來記錄一個線程的讀鎖重入數,以及id;
  • ThreadLocalHoldCounter繼承了ThreadLocal,實現了initialValue方法,作用是在沒有set前調用get的話initialValue會被調用,HoldCounter對象會被存儲到Entry里,并返回它。變量名為readHolds,它用來存儲/獲取線程的讀鎖數量。因為讀鎖是共享的,我們利用同步狀態的高16位來記錄總數,用threadlocal來記錄每個線程所持有的讀鎖數目。對于寫鎖來說它是獨占鎖,低16位代表的就是當前線程持有的寫鎖數目。
  • cachedHoldCounter:它是一種優化的手段,為了避免頻繁的調用ThreadLocalHoldCounter的讀取,更改甚至刪除操作,于是緩存最新一個成功獲取鎖的線程的HoldCounter,意思是當一個線程需要記錄值的時候會先檢查自己是否是cachedHoldCounter中緩存的那個線程,是的話就不用再從readHolds中獲取了,減少對ThreadLocal的操作。
  • firstReader 與firstReaderHoldCount:代表首個獲取讀鎖的線程與其所持有的讀鎖數,該讀鎖數不會存儲進readHolds,這是種優化,針對只有一個線程的情況,避免頻繁操作readHolds。

ReentrantReadWriteLock繼承了ReadWriteLock,這兩個方法分別返回讀鎖與寫鎖。ReentrantReadWriteLock內部實現了兩個類:ReadLock&WriteLock分別實現讀鎖與寫鎖。

public interface ReadWriteLock {
    Lock readLock();

    Lock writeLock();
}

同ReentrantLock一樣內部實現了非公平與公平兩種同步器:NonfairSync &FairSync ,繼承自同一同步器Sync。

    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();
        }
    }

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

只定義了writerShouldBlock & readerShouldBlock兩種方法,它們作用在獲取鎖的過程中,決定當前線程是否該阻塞。

一 讀鎖

1.獲取讀鎖

        public void lock() {
            sync.acquireShared(1);
        }

定位到AQS的acquireShared,該方法之前介紹過。

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

來看看Sync重寫的tryAcquireShared方法

        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 (r == 0) {
                    firstReader = current;
                    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);
        }

寫鎖不為零且持有寫鎖的并非本線程,則返回-1,之后在acquireShared中將線程的節點放入到等待隊列中。寫鎖不為零但是正是本線程持有的,則代表寫讀重入。之后在readerShouldBlock返回false與CAS操作成功后,更新HoldCounter 的值,這里會對之前提到的firstReader ,firstReaderHoldCount 或cachedHoldCounter進行相應的操作。

如果CAS失敗或者readerShouldBlock返回true,則會調用fullTryAcquireShared,該方法會繼續嘗試獲取讀鎖,可以看成是tryAcquireShared的升級版。

先來看看readerShouldBlock()方法:
公平模式下,根據隊列中當前線程之前有沒有等待的線程來決定。

        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }

非公平模式

        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }

調用apparentlyFirstQueuedIsExclusive()

    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
  • 返回true代表等待隊列head.next節點是等待寫鎖的線程,該方法的目的是不讓寫鎖一直等待下去;比如在上一篇自己實現的讀寫鎖中,通過增加一個寫請求變量來防止寫饑餓,讓寫鎖的優先級高于讀鎖。這里有相似的目的。
  • 這個方法是不可靠的,因為在檢測過程中隊列結構是在變化的,;但是我們并不依賴于它的準確表達,它更多是一種探測,一種優化,我們希望它來防止寫鎖的饑餓;而且并不是該方法返回了true,線程就會被放入阻塞隊列退出競爭,來看fullTryAcquireShared的邏輯
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } 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的升級版,它處理了CAS失敗和readerShouldBlock返回true的情況;

  1. 先檢查寫鎖是否被其他線程占用;
  2. 調用readerShouldBlock檢查當前線程是否應該進入等待隊列,返回true也不代表該線程要進入等待隊列,我們看它的處理邏輯:如果firstReader == current代表當前只有你一個讀線程,那么不用等待可以獲取讀鎖;只有在rh.count == 0(意味著該線程沒有持有讀鎖)的情況下返回-1代表線程要進入等待隊列。為什么?持有讀鎖的線程不能進入同步隊列?
  3. 之后便是CAS,成功便更改HoldCounter值返回1,代表獲取讀鎖成功,否則循環再次檢查,總之不能輕易的將線程放入等待隊列,容易造成死鎖。

上面問題的解答:一個線程是不能隨便放入隊列中等待的,容易造成死鎖,看下面兩種情況:

  1. 如果一個線程持有讀鎖,重入失敗被放入等待隊列,若等待隊列中排在它前面的線程里有等待寫鎖的線程,那么就會造成死鎖,因為讀鎖與寫鎖是互斥的。
  2. 假設一個線程持有寫鎖進行“鎖降級申請”,被放入同步隊列,那么不僅之后的讀寫線程都會被放入隊列,隊列中之前有等待線程,無論等待的是讀或寫鎖都將造成死鎖。

回到fullTryAcquireShared

  1. 它先判斷是否有寫鎖,如果有且就是本線程就不會進行readerShouldBlock判斷,直接CAS,這樣便解決了情況2的問題;
  2. 針對情況1線程持有讀鎖的情況,即使readerShouldBlock返回true,rh.count == 0不符合不會返回-1,也就不會將線程放入隊列。

總結就是fullTryAcquireShared,采用for循環方式讓線程不斷判斷與嘗試,且只有在一種情況下才會將線程放入隊列:readerShouldBlock返回true(原因可能是公平模式或者第一個等待線程(head.next)在等待寫鎖),當前線程不是第一個讀線程且沒有持有讀鎖。

2.釋放讀鎖

        public void unlock() {
            sync.releaseShared(1);
        }

定位到AQS的releaseShared,之前介紹過。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

來看看同步器Syn實現的tryReleaseShared

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                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;
            }
            for (;;) {
                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;
            }
        }

1.如果是firstReader ,就對它及firstReader進行修改;2.如果不是,就對readHolds進行修改;3. 自旋CAS修改status
返回true代表status == 0,表示既沒有讀鎖也沒有寫鎖。

3. tryLock

        public boolean tryLock() {
            return sync.tryReadLock();
        }

調用了同步器Syn的tryReadLock

        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 != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

感覺與tryAcquireShared很像,不同在于tryReadLock只嘗試獲取讀鎖一次,成功就返回true,否則false;這是由于方法用途不同,所以設計自然不同。

二, 寫鎖

1,獲取寫鎖

        public void lock() {
            sync.acquire(1);
        }

定位到AQS的acquire中,AQS文章里介紹過。

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

來看看同步器Syn重寫的tryAcquire方法

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

一 :c != 0下分兩種 1. w == 0 代表有讀鎖;2. w != 0 && current != getExclusiveOwnerThread() 代表有其他寫鎖;以上兩種返回false,線程要進入等待隊列。否則就設置status值,返回true,獲取寫鎖成功。
二 :c == 0情況下要看writerShouldBlock的情況,返回false就會去CAS更改同步狀態,成功就將AOS里的exclusiveOwnerThread設置位當前線程,最后返回true;

注:情況一用setState更改同步狀態,情況二用compareAndSetState?情況一執行到setState這步說明當前線程已持有寫鎖,是在重入,其他線程都會被排斥不同擔心線程安全問題,所以setState就可以,同步狀態status是volatile的。情況二里當前即無讀鎖也無寫鎖,當前線程始于其他線程在競爭,所以要利用CAS來保證原子性。
持有讀鎖線程不能申請寫鎖,即不能升級,從tryAcquire可以看出持有讀鎖線程一定會返回false,也就是會被放入隊列中等待寫鎖,但是它持有的讀鎖將不會被釋放,那么寫鎖就不肯能獲取到,雖然不影響讀鎖的獲取,但所有寫鎖都將不能被獲取到。

來看看writerShouldBlock方法
非公平模式:直接返回false,也就是寫請求可以插隊,即寫優先級高;

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

公平模式:考慮的是當前隊列是否有等待的線程

        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }

2,釋放寫鎖

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

定位到AQS中的release

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

來看看同步器Syn重寫的tryRelease

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

注意返回值:返回true代表寫鎖個數為0,也就是寫鎖可用;返回false表示寫鎖仍被當前線程占著,可能是因為當前線程重入了寫鎖。

3,tryLock

        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }

定位到同步器Syn

        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;
        }

tryWriteLock方法看上去跟tryAcquire方法真的很像。唯一的區別在于,tryWriteLock忽略的writerShouldBlock方法;該方法的調用就是去搶寫鎖,搶不到返回false就行了。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容