成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

再次認(rèn)識(shí)ReentrantReadWriteLock讀寫鎖

miya / 2397人閱讀

摘要:但是不管怎樣,在一個(gè)線程已經(jīng)獲取鎖后,在釋放前再次獲取鎖是一個(gè)合理的需求,而且并不生硬。那么如果考慮重入,也很簡(jiǎn)單,在加鎖時(shí)將的值累加即可,表示同一個(gè)線程重入此鎖的次數(shù),當(dāng)歸零,即表示釋放完畢。

前言

最近研究了一下juc包的源碼。
在研究ReentrantReadWriteLock讀寫鎖的時(shí)候,對(duì)于其中一些細(xì)節(jié)的思考和處理以及關(guān)于提升效率的設(shè)計(jì)感到折服,難以遏制想要分享這份心得的念頭,因此在這里寫一篇小文章作為記錄。

本片文章建立在已經(jīng)了解并發(fā)相關(guān)基礎(chǔ)概念的基礎(chǔ)上,可能不會(huì)涉及很多源碼,以思路為主。
如果文章有什么紕漏或者錯(cuò)誤,還請(qǐng)務(wù)必指正,預(yù)謝。

1. 從零開始考慮如何實(shí)現(xiàn)讀寫鎖

首先我們需要知道獨(dú)占鎖(RenentractLock)這種基礎(chǔ)的鎖,在juc中是如何實(shí)現(xiàn)的:
它基于java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)這個(gè)抽象類所搭建的同步框架,利用AQS中的一個(gè)volatile int類型變量的CAS操作來(lái)表示鎖的占用情況,以及一個(gè)雙向鏈表的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)排隊(duì)中的線程。

簡(jiǎn)單地說:一個(gè)線程如果想要獲取鎖,就需要嘗試對(duì)AQS中的這個(gè)volatile int變量(下面簡(jiǎn)稱state)執(zhí)行類似comapre 0 and swap 1的操作,如果不成功就進(jìn)入同步隊(duì)列排隊(duì)(自旋和重入之類的細(xì)節(jié)不展開說了)同時(shí)休眠自己(LockSupport.park),等待占有鎖的線程釋放鎖時(shí)再喚醒它。

那么,如果不考慮重入,state就是一個(gè)簡(jiǎn)單的狀態(tài)標(biāo)識(shí):0表示鎖未被占用,1表示鎖被占用,同步性由volatile和CAS來(lái)保證。

上面說的是獨(dú)占鎖,state可以不嚴(yán)謹(jǐn)?shù)卣J(rèn)為只有兩個(gè)狀態(tài)位。

但是如果是讀寫鎖,那這個(gè)鎖的基本邏輯應(yīng)該是:讀和讀共享讀和寫互斥、寫和寫互斥

如何實(shí)現(xiàn)鎖的共享呢?
如果我們不再把state當(dāng)做一個(gè)狀態(tài),而是當(dāng)做一個(gè)計(jì)數(shù)器,那仿佛就可以說得通了:獲取鎖時(shí)compare n and swap n+1,釋放鎖時(shí)compare n and swap n-1,這樣就可以讓鎖不被獨(dú)占了。

因此,要實(shí)現(xiàn)讀寫鎖,我們可能需要兩個(gè)鎖,一個(gè)共享鎖(讀鎖),一個(gè)獨(dú)占鎖(寫鎖),而且這兩個(gè)鎖還需要協(xié)作,寫鎖需要知道讀鎖的占用情況,讀鎖需要知道寫鎖的占用情況。

設(shè)想中的簡(jiǎn)單流程大概如下:

設(shè)想中的流程很簡(jiǎn)單,然而存在一些問題:

關(guān)于讀寫互斥:

對(duì)于某個(gè)線程,當(dāng)它先獲得讀鎖,然后在執(zhí)行代碼的過程中,又想獲得寫鎖,這種情況是否應(yīng)該允許?

如果允許,會(huì)有什么問題?如果不允許,當(dāng)真的存在這種需求時(shí)怎么辦?

關(guān)于寫寫互斥是否也存在上面兩條提到的情況和問題呢?

我們知道一般而言,讀的操作數(shù)量要遠(yuǎn)遠(yuǎn)大于寫的操作,那么很有可能讀鎖一旦被獲取就長(zhǎng)時(shí)間處于被占有的情況(因?yàn)樾聛?lái)的讀操作只需要進(jìn)去+1就好了,不需要等待state回到0,這樣的話state可能永遠(yuǎn)不會(huì)有回到0的一天),這會(huì)導(dǎo)致極端情況下寫鎖根本沒有機(jī)會(huì)上位,該如何解決這種情況?

對(duì)于上面用計(jì)數(shù)器來(lái)實(shí)現(xiàn)共享鎖的假設(shè),當(dāng)任意一個(gè)線程想要釋放鎖(即使它并未獲取鎖,因?yàn)榻怄i的方法是開放的,任何獲取鎖對(duì)象的線程都可以執(zhí)行lock.unlock())時(shí),如何判斷它是否有權(quán)限執(zhí)行compare n and swap n-1?

是否應(yīng)該使用ThreadLocal來(lái)實(shí)現(xiàn)這種權(quán)限控制?

如果使用ThreadLocal來(lái)控制,如何保證性能和效率?

2. 帶著問題研究ReentrantReadWriteLock

在開始研究ReentrantReadWriteLock之前,應(yīng)當(dāng)先了解兩個(gè)概念:

重入性
一個(gè)很現(xiàn)實(shí)的問題是:我們時(shí)常需要在鎖中加鎖。這可能是由代碼復(fù)用產(chǎn)生的需求,也可能業(yè)務(wù)的邏輯就是這樣。
但是不管怎樣,在一個(gè)線程已經(jīng)獲取鎖后,在釋放前再次獲取鎖是一個(gè)合理的需求,而且并不生硬。
上文在說獨(dú)占鎖時(shí)說到如果不考慮重入的情況,state會(huì)像boolean一樣只有兩個(gè)狀態(tài)位。那么如果考慮重入,也很簡(jiǎn)單,在加鎖時(shí)將state的值累加即可,表示同一個(gè)線程重入此鎖的次數(shù),當(dāng)state歸零,即表示釋放完畢。

公平、非公平
這里的公平和非公平是指線程在獲取鎖時(shí)的機(jī)會(huì)是否公平。
我們知道AQS中有一個(gè)FIFO的線程排隊(duì)隊(duì)列,那么如果所有線程想要獲取鎖時(shí)都來(lái)排隊(duì),大家先來(lái)后到井然有序,這就是公平;而如果每個(gè)線程都不守秩序,選擇插隊(duì),而且還插隊(duì)成功了,那這就是不公平。
但是為什么需要不公平呢?
因?yàn)樾省?br>有兩個(gè)因素會(huì)制約公平機(jī)制下的效率:

上下文切換帶來(lái)的消耗

依賴同步隊(duì)列造成的消耗

我們之所以會(huì)使用鎖、使用并發(fā),可能很大一部分原因是想要挖掘程序的效率,那么相應(yīng)的,對(duì)于性能和效率的影響需要更加敏感。
簡(jiǎn)單地說,上述的兩點(diǎn)由于公平帶來(lái)的性能損耗很可能讓你的并發(fā)失去高效的初衷。
當(dāng)然這也是和場(chǎng)景密切關(guān)聯(lián)的,比如說你非常需要避免ABA問題,那么公平模式很適合你。
具體的不再展開,可以參考這篇文章:深入剖析ReentrantLock公平鎖與非公平鎖源碼實(shí)現(xiàn)

回到我們之前提的問題:

對(duì)于某個(gè)線程,當(dāng)它先獲得讀鎖,然后在執(zhí)行代碼的過程中,又想獲得寫鎖,這種情況是否應(yīng)該允許?

我們先考慮這種情況是否實(shí)際存在:假設(shè)我們有一個(gè)對(duì)象,它有兩個(gè)實(shí)例變量ab,我們需要在實(shí)現(xiàn):if a = 1 then set b = 2,或者換個(gè)例子,如果有個(gè)用戶名字叫張三就給他打錢。

這看上去仿佛是個(gè)CAS操作,然而它并不是,因?yàn)樗婕傲藘蓚€(gè)變量,CAS操作并不支持這種針對(duì)多個(gè)變量的疑似CAS的操作。
為什么不支持呢?因?yàn)閏pu不提供這種針對(duì)多個(gè)變量的CAS操作指令(至少x86不提供),代碼層面的CAS只是對(duì)cpu指令的封裝而已。
為什么cpu不支持呢?可以,但沒必要鄙人也不是特別清楚(逃)。

總而言之這種情況是存在的,但是在并發(fā)情況下如果不加鎖就會(huì)有問題:比如先判斷得到這個(gè)用戶確實(shí)名叫張三,正準(zhǔn)備打錢,突然中途有人把他的名字改了,那再打這筆錢就有問題了,我們判斷名字和打錢這兩個(gè)行為中間應(yīng)當(dāng)是沒有空隙的。
那么為了保證這個(gè)操作的正確性,我們或許可以在讀之前加一個(gè)讀鎖,在我釋放鎖之前,其他人不得改變?nèi)魏蝺?nèi)容,這就是之前所說的讀寫互斥:讀的期間不準(zhǔn)寫。
但是如果照著這個(gè)想法,就產(chǎn)生了自相矛盾的地方:都說了讀期間不能寫,那你自己怎么在寫(打錢)呢?

如果我們順著這個(gè)思路去嘗試解釋“自己讀的期間還在寫”的行為的正當(dāng)性,我們也許可以設(shè)立一個(gè)規(guī)則:如果讀鎖是我自己持有,則我可以寫。
然而這會(huì)出現(xiàn)其他的問題:因?yàn)樽x鎖是共享的,你加了讀鎖,其他人仍然可以讀,這是否會(huì)有問題呢?假如我們的打錢操作涉及更多的值的改變,只有這些值全部改變完畢,才能說此時(shí)的整體狀態(tài)正確,否則在改變完畢之前,讀到的東西都有可能是錯(cuò)的。
再去延伸這個(gè)思路似乎會(huì)變得非常艱難,也許會(huì)陷入耦合的地獄。

但是實(shí)際上我們不需要這樣做,我們只需要反過來(lái)使用讀寫互斥的概念即可:因?yàn)閷憣懟コ猓▽戞i是獨(dú)占鎖),所以我們?cè)趫?zhí)行這個(gè)先讀后寫的行為之前,加一個(gè)寫鎖,這同樣能防止其他人來(lái)寫,同時(shí)還可以阻止其他人來(lái)讀,從而實(shí)現(xiàn)我們?cè)趩尉€程中讀寫并存的需求。

這就是ReentrantReadWriteLock中一個(gè)重要的概念:鎖降級(jí)

對(duì)于另一個(gè)子問題:如果在已經(jīng)獲取寫鎖的期間還要再獲取寫鎖的時(shí)候怎么辦?
這種情況還是很常見的,多數(shù)是由于代碼的復(fù)用導(dǎo)致,不過相應(yīng)的處理也很簡(jiǎn)單:對(duì)寫鎖這個(gè)獨(dú)占鎖增加允許單線程重入的規(guī)則即可。

極端情況下寫操作根本沒有機(jī)會(huì)上位,該如何解決這種情況?

如果我們有兩把鎖,一把讀鎖,一把寫鎖,它們之間想要互通各自加鎖的情況很簡(jiǎn)單——只要去get對(duì)方的state就行了。
但是只知道state是不夠的,對(duì)于讀的操作來(lái)說,它如果只看到寫鎖沒被占用,也不管有多少個(gè)寫操作還在排隊(duì),就去在讀鎖上+1,那很可能發(fā)展成為問題所說的場(chǎng)景:寫操作永遠(yuǎn)沒機(jī)會(huì)上位。

那么我們理想的情況應(yīng)該是:讀操作如果發(fā)現(xiàn)寫鎖空閑,最好再看看寫操作的排隊(duì)情況如何,酌情考慮放棄這一次競(jìng)爭(zhēng),讓寫操作有機(jī)會(huì)上位。
這也是我理解的,為什么ReentrantReadWriteLock不設(shè)計(jì)成兩個(gè)互相溝通的、獨(dú)立的鎖,而是公用一個(gè)鎖(class Sync extends AbstractQueuedSynchronizer)——因?yàn)樗鼈兛此篇?dú)立,實(shí)際上對(duì)于耦合的需求很大,它們不僅需要溝通鎖的情況,還要溝通隊(duì)列的情況。

公用一個(gè)鎖的具體實(shí)現(xiàn)是:使用int state的高16位表示讀鎖的state,低16位表示寫鎖的state,而隊(duì)列公用的方式是給每個(gè)節(jié)點(diǎn)增加一個(gè)標(biāo)記,表明該節(jié)點(diǎn)是一個(gè)共享鎖的節(jié)點(diǎn)(讀操作)還是一個(gè)獨(dú)占鎖的節(jié)點(diǎn)(寫操作)。

上面說到的“酌情放棄這一次競(jìng)爭(zhēng)”,ReentrantReadWriteLock中體現(xiàn)在boolean readerShouldBlock()這個(gè)方法里,這個(gè)方法有兩個(gè)模式:公平非公平,我們來(lái)稍微看一點(diǎn)源碼
先看公平模式的實(shí)現(xiàn)

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

當(dāng)線程發(fā)現(xiàn)自己可以獲取讀鎖時(shí)(寫鎖未被占用),會(huì)調(diào)用這個(gè)方法,來(lái)判斷自己是否應(yīng)該放棄此次獲取。
hasQueuedPredecessors()這個(gè)方法我們不去看源碼,因?yàn)樗囊馑己茱@而易見(實(shí)際代碼也是):是否存在排隊(duì)中的線程(Predecessor先驅(qū)者可以理解為先來(lái)的)。
如果有,那就放棄競(jìng)爭(zhēng)去排隊(duì)。
在公平模式下,無(wú)論讀寫操作,只需要大家都遵守FIFO的秩序,就不會(huì)出現(xiàn)問題描述的情況

再來(lái)看看非公平模式下的實(shí)現(xiàn)代碼:

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

final boolean apparentlyFirstQueuedIsExclusive() {
    // Node表示同步隊(duì)列中的一個(gè)節(jié)點(diǎn)
    Node h, s;
    // head是當(dāng)前隊(duì)列的頭節(jié)點(diǎn)的一個(gè)公共引用,它是一個(gè)沒有實(shí)際意義的節(jié)點(diǎn),null or not只能標(biāo)識(shí)隊(duì)列是否初始化過
    // next是當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)的引用
    // isShared()方法表明這個(gè)節(jié)點(diǎn)是一個(gè)共享鎖(讀鎖)的節(jié)點(diǎn)還是獨(dú)占鎖(寫鎖)的節(jié)點(diǎn)
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

總結(jié)一下語(yǔ)義:如果隊(duì)列不為空,且隊(duì)列最前面的節(jié)點(diǎn)是個(gè)獨(dú)占鎖的節(jié)點(diǎn),則放棄競(jìng)爭(zhēng)。也就是我們上面說的“根據(jù)隊(duì)列情況酌情放棄”。

如何控制讀鎖釋放的權(quán)限?應(yīng)該使用ThreadLocal嗎?它會(huì)對(duì)性能造成影響嗎?

一般而言的讀操作的線程對(duì)于state的操作可能只是+1然后-1,而如果發(fā)生重入,那就會(huì)是n次+1然后n次-1。
但是不管怎樣,每一個(gè)線程都應(yīng)當(dāng)有一份記錄自己持有共享鎖數(shù)量的信息,這樣釋放鎖的時(shí)候才能知道自己可不可以去-1。
這也許很簡(jiǎn)單,我們可以在鎖里增加一個(gè)Map對(duì)象,用類似tid(k)-count(v)的數(shù)據(jù)結(jié)構(gòu)來(lái)記錄每個(gè)線程的持有數(shù)量;也可以為每個(gè)線程創(chuàng)建一個(gè)ThreadLocal,讓它們自己拿著。

現(xiàn)在我們面前有兩條路比較直觀:將所有線程的小計(jì)數(shù)器維護(hù)在一個(gè)Map中,或是每個(gè)線程在ThreadLocal中維護(hù)自己的小計(jì)數(shù)器。
就這兩條途徑而言,應(yīng)該是Map的這一條路比較高效,因?yàn)槿绻x擇ThreadLocal也許會(huì)頻繁進(jìn)行其內(nèi)部的ThreadLocalMap對(duì)象的創(chuàng)建和銷毀,這很消耗資源。

然而事實(shí)是,ReentranctReadWriteLock選擇的實(shí)現(xiàn)方式是后者,即使用ThreadLocal來(lái)實(shí)現(xiàn),但是為什么選擇這種方式正是我十分好奇的地方,因?yàn)楦鶕?jù)經(jīng)驗(yàn),一定是利用Map統(tǒng)一管理小計(jì)數(shù)器的方式較為高效,且單個(gè)線程針對(duì)單個(gè)key的value進(jìn)行+1或者-1的操作應(yīng)該是滿足as-if-serial原則的,也不存在安全問題。
因此針對(duì)兩種不同的實(shí)現(xiàn)方式進(jìn)行了一些測(cè)試:四線程并行情況下一千萬(wàn)次加解鎖時(shí)間測(cè)試

Map統(tǒng)一管理實(shí)現(xiàn)

public static void main(String[] args) {
    long total = 0;
    for (int i = 0; i < 30; i++) {
        total += execute();
    }
    System.out.println(total / 30);
}

private static long execute() {

    var map = new HashMap();
    var readerPool = new ThreadPoolExecutor(
            4,
            4,
            5L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            new CustomizableThreadFactory("readerPool"));

    var countDown = new CountDownLatch(10000000);
    for (int i = 0; i < 10000000; i++) {
        readerPool.execute(() -> {
            try {
                countDown.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            mapImplement(map);
        });
        countDown.countDown();
    }

    long startTime = System.currentTimeMillis();
    while (readerPool.getCompletedTaskCount() < 10000000) {
        LockSupport.parkNanos(100);
    }
    long total = System.currentTimeMillis() - startTime;

    System.out.println(readerPool.getCompletedTaskCount() + ", time: " + total);
    return total;
}

private static void mapImplement(HashMap map) {
    // lock
    var tid = Thread.currentThread().getId();
    Integer count;
    if ((count = map.get(tid)) != null) {
        map.put(tid, count + 1);
    } else {
        map.put(tid, 1);
    }

    // unlock
    int afterDecrement = -999;
    if ((count = map.get(tid)) == null ||
            (afterDecrement = (count - 1)) < 0) {
        System.out.println("error, count: " + count + ", afterDecrement: " + afterDecrement);
        return;
    }
    map.put(tid, afterDecrement);
}

三十次測(cè)試過程的平均執(zhí)行時(shí)間為:2378毫秒,個(gè)人認(rèn)為這個(gè)結(jié)果還是比較樂觀的。

ThreadLocal各自持有實(shí)現(xiàn)

// 小計(jì)數(shù)器實(shí)體
static final class HoldCounter {
    int count;
}

// threadLocal
static final class ThreadLocalHoldCounter extends ThreadLocal {
    @Override
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

public static void main(String[] args) {
    long total = 0;
    for (int i = 0; i < 30; i++) {
        total += execute();
    }
    System.out.println(total / 30);
}

private static long execute() {
    
    var readHolds = new ThreadLocalHoldCounter();
    var readerPool = new ThreadPoolExecutor(
            4,
            4,
            5L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            new CustomizableThreadFactory("readerPool"));

    var countDown = new CountDownLatch(10000000);
    for (int i = 0; i < 10000000; i++) {
        readerPool.execute(() -> {
            try {
                countDown.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            threadLocalImplement(readHolds);
        });
        countDown.countDown();
    }

    long startTime = System.currentTimeMillis();
    while (readerPool.getCompletedTaskCount() < 10000000) {
        LockSupport.parkNanos(100);
    }
    long total = System.currentTimeMillis() - startTime;

    System.out.println(readerPool.getCompletedTaskCount() + ", time: " + total);
    return total;
}

private static void threadLocalImplement(ThreadLocalHoldCounter readHolds) {
    // lock
    var hc = readHolds.get();
    ++hc.count;

    // unlock
    hc = readHolds.get();
    --hc.count;
    if (hc.count == 0) {
        readHolds.remove();
    }
}

三十次測(cè)試過程的平均執(zhí)行時(shí)間為:3079毫秒

可以看到,使用Map集中管理小計(jì)數(shù)器的實(shí)現(xiàn)方式的執(zhí)行效率要比ThreadLocal的實(shí)現(xiàn)方式快20%以上。

難道我作為一個(gè)Java萌新都能想到的性能差距,Doug Lea這樣的大神會(huì)想不到嗎?

當(dāng)然不會(huì)

實(shí)際上,ReentranctReadWriteLock在針對(duì)小計(jì)數(shù)器的具體實(shí)現(xiàn)上,增加了類似兩層緩存的設(shè)計(jì),大概如下:

// ThreadLocal對(duì)象本體
private transient ThreadLocalHoldCounter readHolds;

// 二級(jí)緩存:最近一次獲取鎖的線程所持有的小計(jì)數(shù)器對(duì)象的引用
private transient HoldCounter cachedHoldCounter;

// 一級(jí)緩存:首次獲取鎖的線程的線程對(duì)象引用以及它的計(jì)數(shù)
private transient Thread firstReader;
private transient int firstReaderHoldCount;

當(dāng)線程嘗試獲取鎖時(shí),會(huì)執(zhí)行如下的流程:

判斷當(dāng)前共享鎖總計(jì)數(shù)器是否為0(當(dāng)前鎖處于空閑狀態(tài)) firstReader == Thread.currentThread()
是: 則直接在firstReaderHoldCount上進(jìn)行+1(以及執(zhí)行firstReader = Thread.currentThread()
否: 前往2

判斷cachedHoldCounter.tid == Thread.currentThread().getId()
是: 則直接在cachedHoldCounter.count上進(jìn)行+1
否: 前往3

執(zhí)行readHolds.get()進(jìn)行獲取或初始化,然后再對(duì)小計(jì)數(shù)器進(jìn)行操作

當(dāng)線程釋放鎖時(shí),執(zhí)行流程也大致相似,都是先對(duì)兩級(jí)緩存進(jìn)行嘗試,逼不得已再去對(duì)ThreadLocal進(jìn)行操作。
由于讀操作的實(shí)際執(zhí)行內(nèi)容一般相當(dāng)簡(jiǎn)單(類似return a),所以在絕大多數(shù)情況下,線程的加解鎖行為都會(huì)命中一級(jí)緩存。

我嘗試在ReentranctReadWriteLock的加解鎖行為內(nèi)埋了幾個(gè)計(jì)數(shù)點(diǎn)來(lái)測(cè)試兩級(jí)緩存的命中率,四線程并行1000萬(wàn)次加解鎖操作,結(jié)果是:

一級(jí)緩存命中率大概為90~95%

二級(jí)緩存命中率大概為5~10%

ThreadLocal本體命中率大概為1~5%

而執(zhí)行效率,進(jìn)行1000萬(wàn)次加解鎖,循環(huán)三十次得到的平均執(zhí)行時(shí)間是:2027毫秒
比上面提到的使用Map實(shí)現(xiàn)的方式更要快了15%左右。

雖說上面的小測(cè)試的編碼也好,測(cè)試環(huán)境也好,都不算特別嚴(yán)謹(jǐn),但是還是能非常直觀地說明問題的。

3. ReentranctReadWriteLock實(shí)際設(shè)計(jì)概覽

如圖,ReentranctReadWriteLock中有五個(gè)內(nèi)部類:

Sync
Sync繼承自AbstractQueuedSynchronizer,上文提到的volatile int state以及同步隊(duì)列的實(shí)際實(shí)現(xiàn)都是由AbstractQueuedSynchronizer這個(gè)抽象類提供的,它還提供了一些在鎖的性質(zhì)不同時(shí)實(shí)現(xiàn)也會(huì)不同的可重寫方法,Sync需要做的事情就是將這些通用的方法和規(guī)則加以實(shí)現(xiàn)和擴(kuò)充,形成自己想要實(shí)現(xiàn)的鎖。
Sync也是我們上文提到的,同時(shí)實(shí)現(xiàn)了讀寫兩種性質(zhì)的鎖的根本。
另外,上文提到的關(guān)于分段使用state、利用公平性避免機(jī)會(huì)不均衡的問題、分級(jí)緩存共享鎖小計(jì)數(shù)器等特性,均在此類中實(shí)現(xiàn),需要特別關(guān)注。

NonfairSyncFairSync
這兩個(gè)類都繼承自Sync,它們提供了由Sync定義的兩個(gè)用于進(jìn)行公平性判斷的方法:boolean writerShouldBlock()boolean readerShouldBlock()。實(shí)際使用ReentranctReadWriteLock時(shí),我們會(huì)通過構(gòu)造方法選擇需要構(gòu)造公平還是非公平的鎖,相應(yīng)的會(huì)通過這兩個(gè)子類構(gòu)造實(shí)際的Sync類的對(duì)象,從而影響到加解鎖過程中的一些判斷。

ReadLockWriteLock
這兩個(gè)類都會(huì)持有上面提到的Sync類的對(duì)象的引用,并向用戶(使用者)提供包裝好但實(shí)現(xiàn)不同的操作,比如:

讀鎖獲取

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

寫鎖獲取

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

更多的源碼就不再贅述了,搜索一下就會(huì)有非常多的文章解讀源碼并非作者懶得貼了

后記

juc這套并發(fā)框架的設(shè)計(jì)者和創(chuàng)始人Doug Lea,可以說是java開發(fā)者金字塔頂端的巨佬之一了,他所編寫的juc包的代碼,無(wú)論是代碼結(jié)構(gòu)的合理性、各種設(shè)計(jì)模式的使用、代碼的優(yōu)雅程度都令人嘆為觀止。看完源碼覺得整個(gè)人都升華了

筆者學(xué)習(xí)了源碼之后,覺得在面對(duì)這些充滿了考慮的設(shè)計(jì)細(xì)節(jié)時(shí)產(chǎn)生的思考,才是真正可以使人得到長(zhǎng)遠(yuǎn)的提升的東西。

因此整理出來(lái),作為心得體會(huì)的記錄。如果可以對(duì)其他小伙伴帶來(lái)啟發(fā),那就更好了。
最后,如果文章內(nèi)有什么紕漏或是錯(cuò)誤,還請(qǐng)務(wù)必指正,再次感謝。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/77620.html

相關(guān)文章

  • 再次認(rèn)識(shí)ReentrantReadWriteLock讀寫

    摘要:但是不管怎樣,在一個(gè)線程已經(jīng)獲取鎖后,在釋放前再次獲取鎖是一個(gè)合理的需求,而且并不生硬。那么如果考慮重入,也很簡(jiǎn)單,在加鎖時(shí)將的值累加即可,表示同一個(gè)線程重入此鎖的次數(shù),當(dāng)歸零,即表示釋放完畢。 前言 最近研究了一下juc包的源碼。在研究ReentrantReadWriteLock讀寫鎖的時(shí)候,對(duì)于其中一些細(xì)節(jié)的思考和處理以及關(guān)于提升效率的設(shè)計(jì)感到折服,難以遏制想要分享這份心得的念頭,...

    missonce 評(píng)論0 收藏0
  • J.U.C|讀-寫ReentrantReadWriteLock

    摘要:所以就有了讀寫鎖。只要沒有,讀取鎖可以由多個(gè)線程同時(shí)保持。其讀寫鎖為兩個(gè)內(nèi)部類都實(shí)現(xiàn)了接口。讀寫鎖同樣依賴自定義同步器來(lái)實(shí)現(xiàn)同步狀態(tài)的,而讀寫狀態(tài)就是其自定義同步器的狀態(tài)。判斷申請(qǐng)寫鎖數(shù)量是否超標(biāo)超標(biāo)則直接異常,反之則設(shè)置共享狀態(tài)。 一、寫在前面 在上篇我們聊到了可重入鎖(排它鎖)ReentrantLcok ,具體參見《J.U.C|可重入鎖ReentrantLock》 Reentra...

    Tonny 評(píng)論0 收藏0
  • J.U.C|讀-寫ReentrantReadWriteLock

    摘要:所以就有了讀寫鎖。只要沒有,讀取鎖可以由多個(gè)線程同時(shí)保持。其讀寫鎖為兩個(gè)內(nèi)部類都實(shí)現(xiàn)了接口。讀寫鎖同樣依賴自定義同步器來(lái)實(shí)現(xiàn)同步狀態(tài)的,而讀寫狀態(tài)就是其自定義同步器的狀態(tài)。判斷申請(qǐng)寫鎖數(shù)量是否超標(biāo)超標(biāo)則直接異常,反之則設(shè)置共享狀態(tài)。 一、寫在前面 在上篇我們聊到了可重入鎖(排它鎖)ReentrantLcok ,具體參見《J.U.C|可重入鎖ReentrantLock》 Reentra...

    wendux 評(píng)論0 收藏0
  • 圖解ReentrantReadWriteLock實(shí)現(xiàn)分析

    摘要:鎖實(shí)現(xiàn)分析本節(jié)通過學(xué)習(xí)源碼分析可重入讀寫鎖的實(shí)現(xiàn)。讀寫鎖結(jié)構(gòu)分析繼承于,其中主要功能均在中完成,其中最重要功能為控制線程獲取鎖失敗后轉(zhuǎn)換為等待狀態(tài)及在滿足一定條件后喚醒等待狀態(tài)的線程。 概述 本文主要分析JCU包中讀寫鎖接口(ReadWriteLock)的重要實(shí)現(xiàn)類ReentrantReadWriteLock。主要實(shí)現(xiàn)讀共享,寫互斥功能,對(duì)比單純的互斥鎖在共享資源使用場(chǎng)景為頻繁讀取及少...

    nemo 評(píng)論0 收藏0
  • [Java并發(fā)-10] ReadWriteLock:快速實(shí)現(xiàn)一個(gè)完備的緩存

    摘要:此時(shí)線程和會(huì)再有一個(gè)線程能夠獲取寫鎖,假設(shè)是,如果不采用再次驗(yàn)證的方式,此時(shí)會(huì)再次查詢數(shù)據(jù)庫(kù)。而實(shí)際上線程已經(jīng)把緩存的值設(shè)置好了,完全沒有必要再次查詢數(shù)據(jù)庫(kù)。 大家知道了Java中使用管程同步原語(yǔ),理論上可以解決所有的并發(fā)問題。那 Java SDK 并發(fā)包里為什么還有很多其他的工具類呢?原因很簡(jiǎn)單:分場(chǎng)景優(yōu)化性能,提升易用性 今天我們就介紹一種非常普遍的并發(fā)場(chǎng)景:讀多寫少場(chǎng)景。實(shí)際工作...

    nevermind 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

miya

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<