深入理解讀寫鎖ReentrantReadWriteLock

原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽(yáng)光一路滄桑

詳情請(qǐng)戳www.codercc.com

image

1.讀寫鎖的介紹

在并發(fā)場(chǎng)景中用于解決線程安全的問題厢拭,我們幾乎會(huì)高頻率的使用到獨(dú)占式鎖熏迹,通常使用java提供的關(guān)鍵字synchronized(關(guān)于synchronized可以看這篇文章)或者concurrents包中實(shí)現(xiàn)了Lock接口的ReentrantLock们衙。它們都是獨(dú)占式獲取鎖澎迎,也就是在同一時(shí)刻只有一個(gè)線程能夠獲取鎖。而在一些業(yè)務(wù)場(chǎng)景中,大部分只是讀數(shù)據(jù),寫數(shù)據(jù)很少虐沥,如果僅僅是讀數(shù)據(jù)的話并不會(huì)影響數(shù)據(jù)正確性(出現(xiàn)臟讀),而如果在這種業(yè)務(wù)場(chǎng)景下泽艘,依然使用獨(dú)占鎖的話欲险,很顯然這將是出現(xiàn)性能瓶頸的地方。針對(duì)這種讀多寫少的情況匹涮,java還提供了另外一個(gè)實(shí)現(xiàn)Lock接口的ReentrantReadWriteLock(讀寫鎖)天试。讀寫所允許同一時(shí)刻被多個(gè)讀線程訪問,但是在寫線程訪問時(shí)然低,所有的讀線程和其他的寫線程都會(huì)被阻塞喜每。在分析WirteLock和ReadLock的互斥性時(shí)可以按照WriteLock與WriteLock之間,WriteLock與ReadLock之間以及ReadLock與ReadLock之間進(jìn)行分析脚翘。更多關(guān)于讀寫鎖特性介紹大家可以看源碼上的介紹(閱讀源碼時(shí)最好的一種學(xué)習(xí)方式灼卢,我也正在學(xué)習(xí)中绍哎,與大家共勉)来农,這里做一個(gè)歸納總結(jié):

  1. 公平性選擇:支持非公平性(默認(rèn))和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平崇堰;
  2. 重入性:支持重入沃于,讀鎖獲取后能再次獲取涩咖,寫鎖獲取之后能夠再次獲取寫鎖,同時(shí)也能夠獲取讀鎖繁莹;
  3. 鎖降級(jí):遵循獲取寫鎖檩互,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級(jí)成為讀鎖

要想能夠徹底的理解讀寫鎖必須能夠理解這樣幾個(gè)問題:1. 讀寫鎖是怎樣實(shí)現(xiàn)分別記錄讀寫狀態(tài)的咨演?2. 寫鎖是怎樣獲取和釋放的闸昨?3.讀鎖是怎樣獲取和釋放的?我們帶著這樣的三個(gè)問題薄风,再去了解下讀寫鎖饵较。

2.寫鎖詳解

2.1.寫鎖的獲取

同步組件的實(shí)現(xiàn)聚合了同步器(AQS),并通過重寫重寫同步器(AQS)中的方法實(shí)現(xiàn)同步組件的同步語(yǔ)義(關(guān)于同步組件的實(shí)現(xiàn)層級(jí)結(jié)構(gòu)可以看這篇文章遭赂,AQS的底層實(shí)現(xiàn)分析可以看這篇文章)循诉。因此,寫鎖的實(shí)現(xiàn)依然也是采用這種方式撇他。在同一時(shí)刻寫鎖是不能被多個(gè)線程所獲取茄猫,很顯然寫鎖是獨(dú)占式鎖,而實(shí)現(xiàn)寫鎖的同步語(yǔ)義是通過重寫AQS中的tryAcquire方法實(shí)現(xiàn)的困肩。源碼為:

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    // 1. 獲取寫鎖當(dāng)前的同步狀態(tài)
    int c = getState();
    // 2. 獲取寫鎖獲取的次數(shù)
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 3.1 當(dāng)讀鎖已被讀線程獲取或者當(dāng)前線程不是已經(jīng)獲取寫鎖的線程的話
        // 當(dāng)前線程獲取寫鎖失敗
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 3.2 當(dāng)前線程獲取寫鎖划纽,支持可重復(fù)加鎖
        setState(c + acquires);
        return true;
    }
    // 3.3 寫鎖未被任何線程獲取,當(dāng)前線程可獲取寫鎖
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

這段代碼的邏輯請(qǐng)看注釋僻弹,這里有一個(gè)地方需要重點(diǎn)關(guān)注阿浓,exclusiveCount(c)方法,該方法源碼為:

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

其中EXCLUSIVE_MASK為: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; EXCLUSIVE _MASK為1左移16位然后減1蹋绽,即為0x0000FFFF芭毙。而exclusiveCount方法是將同步狀態(tài)(state為int類型)與0x0000FFFF相與,即取同步狀態(tài)的低16位卸耘。那么低16位代表什么呢退敦?根據(jù)exclusiveCount方法的注釋為獨(dú)占式獲取的次數(shù)即寫鎖被獲取的次數(shù),現(xiàn)在就可以得出來一個(gè)結(jié)論同步狀態(tài)的低16位用來表示寫鎖的獲取次數(shù)蚣抗。同時(shí)還有一個(gè)方法值得我們注意:

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

該方法是獲取讀鎖被獲取的次數(shù)侈百,是將同步狀態(tài)(int c)右移16次,即取同步狀態(tài)的高16位翰铡,現(xiàn)在我們可以得出另外一個(gè)結(jié)論同步狀態(tài)的高16位用來表示讀鎖被獲取的次數(shù)《塾颍現(xiàn)在還記得我們開篇說的需要弄懂的第一個(gè)問題嗎?讀寫鎖是怎樣實(shí)現(xiàn)分別記錄讀鎖和寫鎖的狀態(tài)的锭魔,現(xiàn)在這個(gè)問題的答案就已經(jīng)被我們弄清楚了例证,其示意圖如下圖所示:

讀寫鎖的讀寫狀態(tài)設(shè)計(jì).png

現(xiàn)在我們回過頭來看寫鎖獲取方法tryAcquire,其主要邏輯為:當(dāng)讀鎖已經(jīng)被讀線程獲取或者寫鎖已經(jīng)被其他寫線程獲取迷捧,則寫鎖獲取失斨帧胀葱;否則,獲取成功并支持重入笙蒙,增加寫狀態(tài)抵屿。

2.2.寫鎖的釋放

寫鎖釋放通過重寫AQS的tryRelease方法,源碼為:

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //1. 同步狀態(tài)減去寫狀態(tài)
    int nextc = getState() - releases;
    //2. 當(dāng)前寫狀態(tài)是否為0捅位,為0則釋放寫鎖
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    //3. 不為0則更新同步狀態(tài)
    setState(nextc);
    return free;
}

源碼的實(shí)現(xiàn)邏輯請(qǐng)看注釋轧葛,不難理解與ReentrantLock基本一致,這里需要注意的是艇搀,減少寫狀態(tài)int nextc = getState() - releases;只需要用當(dāng)前同步狀態(tài)直接減去寫狀態(tài)的原因正是我們剛才所說的寫狀態(tài)是由同步狀態(tài)的低16位表示的朝群。

3.讀鎖詳解

3.1.讀鎖的獲取

看完了寫鎖,現(xiàn)在來看看讀鎖中符,讀鎖不是獨(dú)占式鎖姜胖,即同一時(shí)刻該鎖可以被多個(gè)讀線程獲取也就是一種共享式鎖。按照之前對(duì)AQS介紹淀散,實(shí)現(xiàn)共享式同步組件的同步語(yǔ)義需要通過重寫AQS的tryAcquireShared方法和tryReleaseShared方法右莱。讀鎖的獲取實(shí)現(xiàn)方法為:

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果寫鎖已經(jīng)被獲取并且獲取寫鎖的線程不是當(dāng)前線程的話,當(dāng)前
    // 線程獲取讀鎖失敗返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        //2. 當(dāng)前線程獲取讀鎖
        compareAndSetState(c, c + SHARED_UNIT)) {
        //3. 下面的代碼主要是新增的一些功能档插,比如getReadHoldCount()方法
        //返回當(dāng)前獲取讀鎖的次數(shù)
        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;
    }
    //4. 處理在第二步中CAS操作失敗的自旋已經(jīng)實(shí)現(xiàn)重入性
    return fullTryAcquireShared(current);
}

代碼的邏輯請(qǐng)看注釋慢蜓,需要注意的是 當(dāng)寫鎖被其他線程獲取后,讀鎖獲取失敗郭膛,否則獲取成功利用CAS更新同步狀態(tài)晨抡。另外,當(dāng)前同步狀態(tài)需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因這是我們?cè)谏厦嫠f的同步狀態(tài)的高16位用來表示讀鎖被獲取的次數(shù)则剃。如果CAS失敗或者已經(jīng)獲取讀鎖的線程再次獲取讀鎖時(shí)耘柱,是靠fullTryAcquireShared方法實(shí)現(xiàn)的,這段代碼就不展開說了棍现,有興趣可以看看调煎。

3.2.讀鎖的釋放

讀鎖釋放的實(shí)現(xiàn)主要通過方法tryReleaseShared,源碼如下己肮,主要邏輯請(qǐng)看注釋:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 前面還是為了實(shí)現(xiàn)getReadHoldCount等新功能
    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();
        // 讀鎖釋放 將同步狀態(tài)減去讀狀態(tài)即可
        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;
    }
}

4.鎖降級(jí)

讀寫鎖支持鎖降級(jí)士袄,遵循按照獲取寫鎖,獲取讀鎖再釋放寫鎖的次序谎僻,寫鎖能夠降級(jí)成為讀鎖娄柳,不支持鎖升級(jí),關(guān)于鎖降級(jí)下面的示例代碼摘自ReentrantWriteReadLock源碼中:

void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
      }
 
      try {
        use(data);
      } finally {
        rwl.readLock().unlock();
      }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艘绍,一起剝皮案震驚了整個(gè)濱河市赤拒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鞍盗,老刑警劉巖需了,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異般甲,居然都是意外死亡肋乍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門敷存,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墓造,“玉大人,你說我怎么就攤上這事锚烦∶倜觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵涮俄,是天一觀的道長(zhǎng)蛉拙。 經(jīng)常有香客問我,道長(zhǎng)彻亲,這世上最難降的妖魔是什么孕锄? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮苞尝,結(jié)果婚禮上畸肆,老公的妹妹穿的比我還像新娘。我一直安慰自己宙址,他們只是感情好轴脐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抡砂,像睡著了一般大咱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上注益,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天徽级,我揣著相機(jī)與錄音,去河邊找鬼聊浅。 笑死餐抢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的低匙。 我是一名探鬼主播旷痕,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼顽冶!你這毒婦竟也來了欺抗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤强重,失蹤者是張志新(化名)和其女友劉穎绞呈,沒想到半個(gè)月后贸人,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佃声,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年艺智,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圾亏。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡十拣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出志鹃,到底是詐尸還是另有隱情夭问,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布曹铃,位于F島的核電站缰趋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陕见。R本人自食惡果不足惜埠胖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淳玩。 院中可真熱鬧直撤,春花似錦蜕着、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)韧骗。三九已至嘉抒,卻和暖如春袍暴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背政模。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留淋样,地道東北人耗式。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像彪见,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娱挨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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