<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java多線程并發(fā)讀寫鎖ReadWriteLock實(shí)現(xiàn)原理剖析

          共 4777字,需瀏覽 10分鐘

           ·

          2020-08-21 01:59




          轉(zhuǎn)自:搜狐技術(shù)產(chǎn)品??作者:汪建

          本文字?jǐn)?shù):3107

          預(yù)計(jì)閱讀時(shí)間:10分鐘


          關(guān)于讀寫鎖


          Java語法層面的synchronized鎖和JDK內(nèi)置可重入鎖ReentrantLock我們都經(jīng)常會(huì)使用,這兩種鎖都屬于純粹的獨(dú)占鎖,也就是說這些鎖任意時(shí)刻只能由一個(gè)線程持有,其它線程都得排隊(duì)依次獲取鎖。

          為了提高并發(fā)性能我們會(huì)額外引入共享鎖來與獨(dú)占鎖共同對(duì)外構(gòu)成一個(gè)鎖,這種就叫讀寫鎖。

          為什么叫讀寫鎖呢?主要是因?yàn)樗氖褂每紤]了讀寫場(chǎng)景,一般認(rèn)為讀操作不會(huì)改變數(shù)據(jù)所以可以多線程進(jìn)行讀操作,但寫操作會(huì)改變數(shù)據(jù)所以只能一個(gè)線程進(jìn)行寫操作。

          讀寫鎖在內(nèi)部維護(hù)了一對(duì)鎖(讀鎖和寫鎖),它通過將鎖進(jìn)行分離從而得到更高的并發(fā)性能。

          如下圖中,存在一個(gè)讀寫鎖,其內(nèi)部包含了讀鎖和寫鎖兩個(gè)對(duì)象。假如存在五個(gè)線程,其中線程一和線程二想要獲取讀鎖,那么兩個(gè)線程是可以同時(shí)獲取到讀鎖的。但是寫鎖就不可以共享,它是獨(dú)占鎖。

          比如線程三、線程四和線程五都想要持有寫鎖,那么只能一個(gè)個(gè)線程輪著持有。



          讀寫鎖的性質(zhì)


          • 可以多個(gè)線程同時(shí)持有讀鎖,某個(gè)線程成功獲取讀鎖后其它線程仍然能成功獲取讀鎖,即使該線程不釋放讀鎖。


          • 在某個(gè)線程持有讀鎖的情況下其它線程不能持有寫鎖,除非持有讀鎖的線程全部都釋放掉讀鎖。


          • 在某個(gè)線程持有寫鎖的情況下其它線程不能持有寫鎖或讀鎖,某個(gè)線程成功獲取寫鎖后其它所有嘗試獲取讀鎖和寫鎖的線程都將進(jìn)入等待狀態(tài),只有當(dāng)該線程釋放寫鎖后才其它線程能夠繼續(xù)往下執(zhí)行。


          • 如果我們要獲取讀鎖則需要滿足兩個(gè)條件:目前沒有線程持有寫鎖和目前沒有線程請(qǐng)求獲取寫鎖。


          • 如果我們要獲取寫鎖則需要滿足兩個(gè)條件:目前沒有線程持有寫鎖和目前沒有線程持有讀鎖。



          簡(jiǎn)單版本的讀寫鎖


          為了加深對(duì)讀寫鎖的理解,在分析JDK實(shí)現(xiàn)的讀寫鎖之前我們先來看一個(gè)簡(jiǎn)單的讀寫鎖實(shí)現(xiàn)版本。其中三個(gè)整型變量分別表示持有讀鎖的線程數(shù)、持有寫鎖的線程數(shù)以及請(qǐng)求獲取寫鎖的線程數(shù),四個(gè)方法分別對(duì)應(yīng)讀鎖、寫鎖的獲取和釋放操作。

          acquireReadLock方法用于獲取讀鎖,如果持有寫鎖的線程數(shù)量或請(qǐng)求讀鎖的線程數(shù)大于0則讓線程進(jìn)入等待狀態(tài)。

          releaseReadLock方法用于釋放讀鎖,將讀鎖線程數(shù)減一并喚醒其它線程。

          acquireWriteLock方法用于獲取寫鎖,如果持有讀鎖的線程數(shù)量或持有寫鎖的線程數(shù)量大于0則讓線程進(jìn)入等待狀態(tài)。

          releaseWriteLock方法用于釋放寫鎖,將寫鎖線程數(shù)減一并喚醒其它線程。



          讀鎖升級(jí)為寫鎖


          在某些場(chǎng)景下,我們希望某個(gè)已經(jīng)擁有讀鎖的線程能夠獲得寫鎖,并將原來的讀鎖釋放掉,這種情況就涉及到讀鎖升級(jí)為寫鎖操作。讀寫鎖的升級(jí)操作需要滿足一定的條件,這個(gè)條件就是某個(gè)線程必須是唯一擁有讀鎖的線程,否則將無法成功升級(jí)。

          如下圖中,線程二已經(jīng)持有讀鎖了,而且它是唯一的一個(gè)持有讀鎖的線程,所以它可以成功獲得寫鎖。



          寫鎖降級(jí)為讀鎖


          與鎖升級(jí)相對(duì)應(yīng)的是鎖降級(jí),鎖降級(jí)就是某個(gè)已經(jīng)擁有寫鎖的線程希望能夠獲得讀鎖,并將原來的寫鎖釋放掉。鎖降級(jí)操作幾乎沒有什么風(fēng)險(xiǎn),因?yàn)閷戞i是獨(dú)占鎖,持有寫鎖的線程肯定是唯一的,而且讀鎖也肯定不存在持有線程,所以寫鎖可以直接降級(jí)為讀鎖。

          如下圖中,線程三持有寫鎖,此時(shí)其它線程不可能持有讀鎖和寫鎖,所以可以安全地將寫鎖降為讀鎖。



          ReentrantReadWriteLock類圖


          為了更清晰理解ReentrantReadWriteLock類的結(jié)構(gòu),我們先來看它的類圖。該類包含的屬性和方法較多,為了讓分析思路清晰且方便讀者理解,我們將剔除非核心源碼,只對(duì)核心功能進(jìn)行分析。

          ReentrantReadWriteLock類實(shí)現(xiàn)了Serializable接口和ReadWriteLock接口,前者用于序列化,而后者則提供了readLock()和writeLock()兩個(gè)方法。該類的內(nèi)部同步器Sync基于AQS同步器實(shí)現(xiàn),即繼承了AbstractQueuedSynchronizer類。

          同步器分為公平模式和非公平模式,分別對(duì)應(yīng)著FairSync類和NonfairSync類。其中公平/非公平模式表示多個(gè)線程同時(shí)去獲取鎖時(shí)是否按照先到先得的順序獲得鎖,如果是則為公平模式,否則為非公平模式。

          該類還包含了讀鎖和寫鎖,分別對(duì)應(yīng)ReadLock類和WriteLock類,它們都屬于ReentrantReadWriteLock的內(nèi)部類,且都集成了Lock接口。



          ReentrantReadWriteLock實(shí)現(xiàn)思想


          總的來說,ReentrantReadWriteLock類中不管是公平模式還是非公平模式、不管是讀鎖還是寫鎖都是基于AQS同步器來實(shí)現(xiàn)的。實(shí)現(xiàn)的主要難點(diǎn)在于只使用一個(gè)AQS同步器對(duì)象來實(shí)現(xiàn)讀鎖和寫鎖,這就要求讀鎖和寫鎖共用同一個(gè)共享狀態(tài)變量。


          我們知道AQS同步器的共享狀態(tài)是整型的,即32位,那么最簡(jiǎn)單的共用方式就是讀鎖和寫鎖分別使用16位。其中高16位用于讀鎖的狀態(tài),而低16位則用于寫鎖的狀態(tài),這樣便達(dá)到共用效果。

          但是這樣設(shè)計(jì)后當(dāng)我們要獲取讀鎖和寫鎖的狀態(tài)值時(shí)則需要一些額外的計(jì)算,比如一些移位和邏輯與操作。


          ReentrantReadWriteLock的同步器共用狀態(tài)變量的邏輯如下,其中SHARED_SHIFT表示移動(dòng)的位數(shù)為16;SHARED_UNIT表示讀鎖每次加鎖對(duì)應(yīng)的狀態(tài)值大小,1左移16位剛好對(duì)應(yīng)高16位的1;MAX_COUNT表示讀鎖能被加鎖的最大次數(shù),值為16個(gè)1(二進(jìn)制);EXCLUSIVE_MASK表示寫鎖的掩碼,值為16個(gè)1(二進(jìn)制)。

          sharedCount方法用于獲取讀鎖(高16位)的狀態(tài)值,左移16位即能得到。exclusiveCount方法用于獲取寫鎖(低16位)的狀態(tài)值,通過掩碼即能得到。



          ReadLock與WriteLock


          ReadLock與WriteLock是ReentrantReadWriteLock的兩個(gè)要素,它們都實(shí)現(xiàn)了Lock接口,我們主要關(guān)注lock、unlock和newCondition這三個(gè)核心方法。

          分別表示對(duì)讀鎖和寫鎖的加鎖操作、釋放鎖操作和創(chuàng)建Condition對(duì)象操作,可以看到這些方法都間接調(diào)用了同步器Sync的方法,需要注意的是讀鎖不支持創(chuàng)建Condition對(duì)象。



          公平/非公平模式


          ReentrantReadWriteLock的默認(rèn)模式為非公平模式,其中同步器Sync是公平模式FairSync類和非公平模式NonfairSync類的抽象父類。

          因?yàn)镽eentrantReadWriteLock的讀鎖使用了共享模式,而寫鎖使用了獨(dú)占模式,所以該父類將不同模式下的公平機(jī)制抽象為readerShouldBlock和writerShouldBlock兩個(gè)抽象方法,然后子類就可以各自實(shí)現(xiàn)不同的公平模式。

          換句話說,ReentrantReadWriteLock的公平機(jī)制就由這兩個(gè)方法來決定了。


          下面就是公平模式和非公平模式的差異,公平模式的readerShouldBlock和writerShouldBlock兩個(gè)方法都直接返回hasQueuedPredecessors方法的結(jié)果,表示如果等待隊(duì)列中有其它線程則讓當(dāng)前線程加入等待隊(duì)列中,從而保證公平性。

          而非公平模式的writerShouldBlock方法直接返回false,表示不讓當(dāng)前線程進(jìn)入等待隊(duì)列,而是直接進(jìn)行鎖的獲取競(jìng)爭(zhēng)。readerShouldBlock方法則調(diào)用apparentlyFirstQueuedIsExclusive方法判斷頭結(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)線程是否在請(qǐng)求獲取獨(dú)占鎖(寫鎖)。

          如果是則讓其它線程先獲取寫鎖,而自己則乖乖去排隊(duì)。如果不是則說明下一個(gè)節(jié)點(diǎn)線程是請(qǐng)求共享鎖(讀鎖),此時(shí)直接與之競(jìng)爭(zhēng)讀鎖。



          寫鎖的實(shí)現(xiàn)


          WriteLock有兩個(gè)核心方法:lock和unlock。它們都會(huì)間接調(diào)用內(nèi)部同步器sync對(duì)應(yīng)方法,在同步器中需要重寫tryAcquire方法和tryRelease方法,分別用于獲取寫鎖和釋放寫鎖操作。

          先看tryAcquire方法的邏輯,c!=0時(shí)有兩種情況,一種是高16位的讀鎖狀態(tài)不為0,一種是低16位的寫鎖狀態(tài)不為0。w等于0時(shí)表示還有線程持有讀鎖,直接返回false表示獲取寫鎖失敗。

          如果持有寫鎖的線程為當(dāng)前線程,則表示寫鎖重入操作,此時(shí)需要將狀態(tài)變量進(jìn)行累加,此外需要校驗(yàn)的是寫鎖重入狀態(tài)值不能超過MAX_COUNT。

          通過writerShouldBlock方法判斷是否需要將當(dāng)前線程放入排隊(duì)隊(duì)列中,同時(shí)通過CAS方式對(duì)狀態(tài)變量進(jìn)行累加操作。

          對(duì)于非公平模式,這里就是闖入操作,即線程先嘗試一次競(jìng)爭(zhēng)寫鎖。最后設(shè)置當(dāng)前線程持有寫鎖。

          繼續(xù)看tryRelease方法的邏輯,先用isHeldExclusively方法檢查當(dāng)前線程必須為寫鎖持有線程,然后將狀態(tài)值減去釋放的值并獲取寫鎖狀態(tài)值,如果其值為0則表示不存在重入情況,可以徹底釋放鎖了。

          設(shè)置無線程持有寫鎖,最后設(shè)置新的狀態(tài)值。



          讀鎖的實(shí)現(xiàn)


          讀鎖需重寫同步器中的tryAcquireShared方法和tryReleaseShared方法,它們分別用于獲取讀鎖和釋放讀鎖操作。

          tryAcquireShared方法的邏輯為:先獲取寫鎖狀態(tài),如果不為0則表示有其它線程持有寫鎖而且當(dāng)前線程沒有持有寫鎖,則此時(shí)嘗試獲取讀鎖失敗,將當(dāng)前線程放到排隊(duì)隊(duì)列。

          注意這里如果當(dāng)前線程持有寫鎖的話則可以繼續(xù)獲取讀鎖。然后獲取讀鎖狀態(tài),嘗試通過CAS設(shè)置新的狀態(tài)值,如果成功則返回1表示成功獲取讀鎖。如果不成功則繼續(xù)調(diào)用fullTryAcquireShared方法,該方法主要是一個(gè)自旋操作。

          如果寫鎖不為0且當(dāng)前線程未持有寫鎖則返回-1,表示嘗試獲取讀鎖失敗,將當(dāng)前線程加入排隊(duì)隊(duì)列中。如果寫鎖的狀態(tài)為0,則表示沒有線程持有寫鎖,繼續(xù)通過readerShouldBlock方法判斷是否需要將該線程加入到排隊(duì)隊(duì)列中。

          此外,讀鎖的狀態(tài)值不能等于MAX_COUNT,最后通過CAS方式設(shè)置新的狀態(tài)值。

          tryReleaseShared方法的邏輯為:通過for循環(huán)實(shí)現(xiàn)自旋,自旋的邏輯就是不斷計(jì)算新的狀態(tài)值,然后通過CAS方式來設(shè)置新的狀態(tài)值。



          一個(gè)例子


          接著我們看一個(gè)讀寫鎖的例子,看看如何使用讀寫鎖。首先創(chuàng)建ReentrantReadWriteLock對(duì)象,然后通過它的讀鎖和寫鎖來訪問線程不安全的TreeMap對(duì)象。

          其中g(shù)et方法屬于讀取數(shù)據(jù)的操作,所以使用共享的讀鎖即可。

          而put和clear兩個(gè)方法涉及到修改數(shù)據(jù)的操作,需要使用獨(dú)占的寫鎖。



          總結(jié)


          本文介紹了Java中的讀寫鎖ReentrantReadWriteLock的相關(guān)知識(shí),并深入講解了它的實(shí)現(xiàn)原理。在ReentrantReadWriteLock讀寫鎖中,寫鎖是一種獨(dú)占鎖,包括了公平模式和非公平模式。

          而讀寫則是一種共享鎖,它也包含了公平模式和非公平模式。它的實(shí)現(xiàn)基于AQS同步器,其中最重要的點(diǎn)是它通過某些技巧讓讀鎖和寫鎖公共了同一個(gè)狀態(tài)變量。通過本文的講解相信大家已經(jīng)很好地掌握了JDK提供的讀寫鎖的實(shí)現(xiàn)原理。





          我整理了一份很全的學(xué)習(xí)資料,感興趣的可以微信搜索?猿天地?」,回復(fù)關(guān)鍵字 「學(xué)習(xí)資料?」獲取我整理好了的Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC分庫分表,任務(wù)調(diào)度框架XXL-JOB,MongoDB,爬蟲等相關(guān)資料。



          后臺(tái)回復(fù)?學(xué)習(xí)資料?領(lǐng)取學(xué)習(xí)資料


          如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99精品大香蕉 | 天天天天爽爽天干 | 国产精品一级无码免费播放 | 黑人大屌与欧美成人视频 | 日本在线观看中 |