ReentrantReadWriteLock代碼淺析

介紹

除了重入鎖ReentrantLock以外它碎,Doug Lea大神還順帶實現(xiàn)了讀寫重入鎖ReentrantReadWriteLock坠宴,依舊支持重入特性、公平與非公平模式,分出了讀鎖和寫鎖叼丑。

讀鎖

當讀鎖被持有后,會阻止其他線程獲取寫鎖掠河,但讀鎖不排斥其他線程持有讀鎖栈虚。

寫鎖

當寫鎖被持有后,既排斥其他線程申請讀鎖汁掠,還排斥其他線程申請寫鎖略吨。

結(jié)構(gòu)

具體的結(jié)構(gòu)關系,直接上圖:


流程

通過ReentrantReadWriteLock的readLock()獲取讀鎖考阱,writeLock()獲取寫鎖翠忠,讀鎖和寫鎖都有l(wèi)ock()、lockInterruptibly()乞榨、tryLock()秽之、tryLock(long timeout, TimeUnit unit)当娱,unlock()。

lock()方法考榨,遵循初始化時的公平鎖或非公平鎖模式請求鎖跨细,請求鎖的過程不會被中斷,直到持有鎖為止河质。

lockInterruptibly()方法冀惭,如果線程調(diào)用了interrupted(),請求鎖的過程將會中斷愤诱,并且拋出InterruptedException異常云头。如果線程沒有被中斷,那么會堅持到持有鎖為止淫半。

tryLock()方法溃槐,無視初始化時公平與非公平模式,直接嘗試一次請求鎖的CAS操作科吭,如果成功昏滴,返回true,失敗对人,返回false谣殊。

tryLock(long timeout, TimeUnit unit)方法,類似lockInterruptibly()牺弄,但是比lockInterruptibly()多了超時檢查操作姻几,當超時時,會中斷請求鎖過程势告。
unlock()方法蛇捌,釋放鎖對象當前持有的鎖一次。

申請讀鎖

因為請求寫鎖鎖的流程與普通重入鎖大同小異咱台,所以這里只展示讀鎖的lock()方法的請求流程络拌,lock()方法中通過sync調(diào)用acquireShared()方法,acquireShared()方法的實現(xiàn)在AbstractQueuedSynchronizer中:

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

所有讀鎖會進行一次tryAcquireShared(int arg)doAcquireShared(int arg)回溺。獲取寫鎖則調(diào)用acquire(int arg)方法春贸,方法中if條件中調(diào)用tryAcquire(arg)acquireQueued(final Node node, int arg),與讀鎖獲取的流程有些小差別遗遵。

回到正題萍恕,tryAcquireShared(int arg)方法被Sync實現(xiàn),再看ReentrantReadWriteLock.Sync中:

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

這個方法中有兩個比較重要的流程车要,都會在特定條件下返回雄坪,先看看進入這兩個流程的if條件。
第一個if條件,exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)维哈,先檢查寫鎖重入數(shù)绳姨,如果不為0,再檢查當前線程是否已經(jīng)持有寫鎖阔挠,如果未持有寫鎖則返回-1表示失敗飘庄,那么可以看出,同一個線程是可以同時持有寫鎖和讀鎖的购撼。

第二個if條件跪削,!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)
首先如果是公平鎖,那么在readerShouldBlock()中會進入阻塞隊列等待迂求,如果非公平鎖碾盐,就調(diào)用apparentlyFirstQueuedIsExclusive()方法檢查隊列第一個節(jié)點是否在等待寫鎖,如果不是揩局,再來檢查讀鎖持有是否超過最大值毫玖,如果未超過,再來CAS修改讀鎖持有數(shù)量凌盯,CAS操作成功之后付枫,這個if流程中會修改部分引用和緩存信息,這個具體作用后面再說驰怎。

在公平鎖模式阻塞重新被喚醒后或者已有其他線程持有讀鎖阐滩,又或者讀鎖當前持有數(shù)現(xiàn)在已經(jīng)到了MAX_COUNT時,都會跳過第二個if流程直接進入fullTryAcquireShared(Thread current)方法中:

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.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            //省略部分代碼
            }
            return 1;
        }
    }
}

fullTryAcquireShared(Thread current)方法中的請求鎖的過程其實和tryAcquireShared(int unused)中的過程十分相似症杏,最重要的還是CAS操作的這個方法衅疙。只有compareAndSetState()成功之后才會結(jié)束fullTryAcquireShared(Thread current)方法中for循環(huán),結(jié)束這次讀鎖請求操作鸳慈。

exclusiveCount(int c)與sharedCount(int c)

fullTryAcquireShared(Thread current)方法與tryAcquireShared(int unused)方法中中,變量c喧伞,就是持有鎖的個數(shù)走芋,通過getState()獲取到值后,先exclusiveCount(int c)潘鲫,再經(jīng)過sharedCount(int c)后賦值給r翁逞,找到和它們有關的代碼:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);// 65536
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;// 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 65535
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

exclusiveCount(int c)操作會清除整型變量c高16位的所有數(shù)值,保留低16位數(shù)值溉仑。
sharedCount(int c) 操作清除整型變量c低16位挖函,保留高16位數(shù)值。
所以r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)就是在驗證寫鎖持有數(shù)量浊竟,當c = 0c + SHARED_UNIT = 65536怨喘,經(jīng)過sharedCount(int c)計算后得1津畸,c + SHARED_UNIT相當于對 c = (c >>> 16 + 1) << 16,對c高16位數(shù)值加1必怜。
也就是說state值的高位保存著讀鎖數(shù)量肉拓,低位保存寫鎖數(shù)量,讀鎖與讀鎖最大持有數(shù)都是65535梳庆。

關于讀寫鎖等待隊列

不管是公平鎖還是非公平鎖暖途,未能在其他線程釋放鎖時的特殊時刻正好獲取到鎖,都會進入等待鎖隊列膏执,而這個隊列被讀鎖與寫鎖共用驻售,具體邏輯很多與重入鎖高度重合,有興趣可以看我寫的這篇博客ReentrantLock源碼通讀(一)更米。

關于HoldCounter

HoldCounter遍布在所有讀鎖的請求和釋放過程當中欺栗,并且為了緩存這個讀鎖持有數(shù),代碼占據(jù)了這些流程代碼量大概兩成左右壳快,所以HoldCounter的重要性不言而喻纸巷,那么具體是什么作用呢?
在ReentrantReadWriteLock中對外提供了一個方法:

/**
 * Queries the number of reentrant read holds on this lock by the
 * current thread.  A reader thread has a hold on a lock for
 * each lock action that is not matched by an unlock action.
 *
 * @return the number of holds on the read lock by the current thread,
 *         or zero if the read lock is not held by the current thread
 * @since 1.6
 */
public int getReadHoldCount() {
    return sync.getReadHoldCount();
}

//Sync中getReadHoldCount()
final int getReadHoldCount() {
    if (getReadLockCount() == 0)
        return 0;

    Thread current = Thread.currentThread();
    if (firstReader == current)
        return firstReaderHoldCount;

    HoldCounter rh = cachedHoldCounter;
    if (rh != null && rh.tid == getThreadId(current))
        return rh.count;

    int count = readHolds.get().count;
    if (count == 0) readHolds.remove();
    return count;
}

注釋翻譯過來就是眶痰,查詢當前線程重入讀鎖的次數(shù)瘤旨。不是可以通過Sync中的state全局變量獲取重入次數(shù)嗎,讀鎖與寫鎖是通過int的高低位來區(qū)分的竖伯,十分方便存哲,為什么不能用這個state來獲取讀鎖的重入次數(shù)呢?
事實上寫鎖的確是利用state的來獲取寫鎖的重入次數(shù)的七婴,但是寫鎖是一個線程獨占的祟偷,它會排斥其他線程獲取寫鎖以及讀鎖。
讀鎖打厘,因為其可被共享的特殊性質(zhì)修肠,導致state中高位存儲的是多個線程重入后的總數(shù),所以state無法用來查詢當前線程重入讀鎖的次數(shù)户盯,當前線程無法知曉自己重入次數(shù)嵌施,這在某些復雜場景或者使用不規(guī)范的情況下,可能造成讀鎖無法被正常釋放莽鸭,而且排查起來也異常困難吗伤。
所以HoldCounter出現(xiàn)了,與HoldCounter配合當然少不了ThreadLocal硫眨,ReentrantReadWriteLock.Sync中包含有HoldCounter靜態(tài)內(nèi)部類和ThreadLocalHoldCounter靜態(tài)內(nèi)部類足淆。
HoldCounter記錄了重入計數(shù)與線程的tid。
ThreadLocalHoldCounter繼承ThreadLocal,重載了initialValue()方法巧号。
并且為了更快的獲取重入數(shù)族奢,還額外贈送了兩個全局變量,cachedHoldCounter和firstReaderHoldCount裂逐。
cachedHoldCounter保存上個請求讀鎖線程的HoldCounter歹鱼。
firstReaderHoldCount保存第一個獲取讀鎖線程的重入數(shù)。

所以Sync實現(xiàn)的getReadHoldCount()方法根據(jù)return卜高,分為四個分支弥姻,通過各種全局緩存變量,盡量高效的返回結(jié)果掺涛。
第一個分支庭敦,讀取state的高位,如果為0返回薪缆。
第二個分支秧廉,檢查當前線程是否等于firstReader,如果是拣帽,返回firstReaderHoldCount疼电。
第三個分支,檢查cachedHoldCounter中的tid是否等于當前線程的tid减拭,如果是蔽豺,返回cachedHoldCounter.count
第四個分支拧粪,就是遍歷ThreadLocal中的threadMap找到當前線程的HoldCounter返回count修陡。

思考

一個線程是否可以同時獲取到寫鎖與讀鎖?
肯定可以可霎,不然也不會在注釋里說明會產(chǎn)生死鎖的場景了魄鸦。
根據(jù)寫鎖的tryAcquire(int arg)源碼:

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

先判斷了state != 0,再判斷w==0 || current != getExclusiveOwnerThread()直接堵死線程獲取讀鎖在獲取寫鎖的可能性癣朗。
所以能同時獲取讀鎖和寫鎖的操作只能是先獲取寫鎖拾因,然后獲取讀鎖。
如何能成功同時獲取讀鎖和寫鎖呢旷余?
在非公平模式下绢记,線程持有寫鎖后,去請求讀鎖荣暮,而此時等待線程隊列中第一個節(jié)點不是請求寫鎖的線程,即可成功獲取讀鎖與寫鎖罩驻,但如果是請求寫鎖的線程穗酥,會返回失敗狀態(tài)值-1,之后進入等待線程隊列,線程在持有寫鎖的情況下休眠并且等待通知被喚醒砾跃,由于喚醒超時線程的觸發(fā)點實在有線程釋放鎖之后骏啰,所以這時無論是否設置過超時參數(shù),都是無效的抽高,依然會導致死鎖判耕,所以在使用時需要格外注意。

總結(jié)

讀鎖可以與其他線程共享翘骂,但排斥其他線程獲取寫鎖壁熄。
寫鎖排斥其他線程獲取鎖,當前持有寫鎖后碳竟,仍然可以去獲取讀鎖草丧,完全獨占,但是寫鎖競爭激烈時莹桅,完全獨占的操作也很容易導致死鎖昌执。
讀寫鎖的持有或重入上限均是65535。
當前線程重入計數(shù)诈泼,讀寫鎖是分開保存的懂拾,state中保存的是讀鎖重入的總數(shù),寫鎖不受影響铐达。
當前線程讀鎖重入次數(shù)是使用ThreadLocal保存的岖赋,每個線程單獨維護一個HoldCounter。使用的三個全局變量緩存特殊狀態(tài)的線程重入計數(shù)變量娶桦,盡量快的返回結(jié)果贾节。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市衷畦,隨后出現(xiàn)的幾起案子栗涂,更是在濱河造成了極大的恐慌,老刑警劉巖祈争,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斤程,死亡現(xiàn)場離奇詭異,居然都是意外死亡菩混,警方通過查閱死者的電腦和手機忿墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沮峡,“玉大人疚脐,你說我怎么就攤上這事⌒细恚” “怎么了棍弄?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵望薄,是天一觀的道長。 經(jīng)常有香客問我呼畸,道長痕支,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任蛮原,我火速辦了婚禮卧须,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘儒陨。我一直安慰自己花嘶,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布框全。 她就那樣靜靜地躺著察绷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪津辩。 梳的紋絲不亂的頭發(fā)上拆撼,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音喘沿,去河邊找鬼闸度。 笑死,一個胖子當著我的面吹牛蚜印,可吹牛的內(nèi)容都是我干的莺禁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼窄赋,長吁一口氣:“原來是場噩夢啊……” “哼哟冬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起忆绰,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤浩峡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后错敢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翰灾,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年稚茅,在試婚紗的時候發(fā)現(xiàn)自己被綠了纸淮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡亚享,死狀恐怖咽块,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情欺税,我是刑警寧澤侈沪,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布飒货,位于F島的核電站,受9級特大地震影響峭竣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晃虫,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一皆撩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哲银,春花似錦扛吞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至做院,卻和暖如春盲泛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背键耕。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工寺滚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人屈雄。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓村视,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酒奶。 傳聞我的和親對象是個殘疾皇子蚁孔,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容