圖解ReentrantLock底層公平鎖和非公平鎖實現(xiàn)原理

image

??在面試或者日常開發(fā)當(dāng)中盼玄,經(jīng)常會遇到公平鎖和非公平鎖的概念。

兩者最大的區(qū)別如下??

1?? 公平鎖:N個線程去申請鎖時埃儿,會按照先后順序進(jìn)入一個隊列當(dāng)中去排隊童番,依次按照先后順序獲取鎖。就像下圖描述的上廁所的場景一樣剃斧,先來的先占用廁所幼东,后來的只能老老實實排隊。

image

2?? 非公平鎖:N個線程去申請鎖策橘,會直接去競爭鎖娜亿,若能獲取鎖就直接占有,獲取不到鎖沛婴,再進(jìn)入隊列排隊順序等待獲取鎖。同樣以排隊上廁所打比分泻蚊,這時候丑婿,后來的線程會先嘗試插隊看看能否搶占到廁所,若能插隊搶占成功秒旋,就能使用廁所诀拭,若失敗就得老老實實去隊伍后面排隊耕挨。


image

針對這兩個概念,我們通過ReentrantLock底層源碼來分析下?? :公平鎖和非公平鎖在ReentrantLock類當(dāng)中鎖怎樣實現(xiàn)的贪庙。

??ReentrantLock內(nèi)部實現(xiàn)的公平鎖類是FairSync赋铝,非公平鎖類是NonfairSync。

當(dāng)ReentrantLock以無參構(gòu)造器創(chuàng)建對象時,默認(rèn)生成的是非公平鎖對象NonfairSync良哲,只有帶參且參數(shù)為true的情況下FairSync助隧,才會生成公平鎖,若傳參為false時巍实,生成的依然是非公平鎖哩牍,兩者構(gòu)造器源碼結(jié)果如下??


image

圖1

在實際開發(fā)當(dāng)中膝昆,關(guān)于ReentrantLock的使用案例叠必,一般是這個格式??

 class X {    
   private final ReentrantLock lock = new ReentrantLock();    
   // ...      
   public void m() {      
     lock.lock();  
     // block until condition holds      
     try {        
       // ... method body      
     } finally {        
       lock.unlock()      
     }    
   }  
 }

這時的lock指向的其實是NonfairSync對象纬朝,即非公平鎖骄呼。

當(dāng)使用lock.lock()對臨界區(qū)進(jìn)行占鎖操作時,最終會調(diào)用到NonfairSync對象的lock()方法俄讹。根據(jù)圖1可知绕德,NonfairSync和FairSync兩者的lock方法實現(xiàn)邏輯是不一樣的,而體現(xiàn)其鎖是否符合公平與否的地方踪蹬,就是在兩者的lock方法里臣咖。

image

可以看到夺蛇,在非公平鎖NonfairSync的上鎖lock方法當(dāng)中,若if(compareAndSetState(0,1))判斷不滿足娶聘,就會執(zhí)行acquire(1)方法甚脉,該方法跟公平鎖FairSync的lock方法里調(diào)用的acquire(1)其實是同一個,但方法里的tryAcquire具體實現(xiàn)又存在略微不同狡耻,這里后面會討論猴凹。

這里就呼應(yīng)前文提到的非公平鎖的概念——當(dāng)N個線程去申請非公平鎖郊霎,它們會直接去競爭鎖,若能獲取鎖就直接占有歹篓,獲取不到鎖,再進(jìn)入隊列排隊順序等待獲取鎖背捌。這里的“獲取不到鎖毡庆,再進(jìn)入隊列排隊順序等待獲取鎖”可以理解成?——當(dāng)線程過來直接競爭鎖失敗后,就會變成公平鎖的形式毅否,進(jìn)入到一個隊列當(dāng)中蝇刀,按照先后順序排隊去獲取鎖。

而if(compareAndSetState(0,1))語句塊的邏輯捆探,恰好就體現(xiàn)了“當(dāng)N個線程去申請非公平鎖站粟,它們會直接去競爭鎖,若能獲取鎖就直接占有”這句話的意思助被。

??首先切诀,先來分析NonfairSync的lock()方法原理趾牧,源碼如下??

final void lock() {
  //先競爭鎖,若能競爭成功,則占有鎖資源
    if (compareAndSetState(0, 1))
      //將獨占線程成員變量設(shè)置為當(dāng)前線程蹦渣,表示占有鎖資源的線程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

compareAndSetState(0, 1)就是一個當(dāng)前線程與其他線程搶占鎖的過程柬唯,這里面涉及到AQS的知識點,因此锄奢,閱讀本文時,需具備一定的AQS基礎(chǔ)涂屁。

JUC的鎖實現(xiàn)是基于AQS實現(xiàn)的拆又,可以簡單理解成,AQS里定義了一個private volatile int state變量栈源,若state值為0竖般,說明無線程占有,其他線程可以進(jìn)行搶占鎖資源艰亮;若state值為1胞谭,說明已有線程占有鎖資源丈屹,其他線程需要等待該占有鎖的線程釋放鎖資源后,方能進(jìn)行搶占鎖的動作彩库。


image

線程在搶占鎖時先蒋,是通過CAS對state變量進(jìn)行置換操作的,期望值expect是0眯搭,更新值update為1业岁,若期望值expect能與內(nèi)存地址里的state值一致笔时,就可以通過原子操作將內(nèi)存地址里state值置換成更新值update,返回true借笙,反之,就置換失敗返回false盗痒。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

可見盼忌,這里的if (compareAndSetState(0, 1))就體現(xiàn)了非公平鎖的機制,當(dāng)前線程會先去競爭鎖看成,若能競爭成功跨嘉,就占有鎖資源祠乃。

若競爭鎖失敗話,就會執(zhí)行acquire(1)方法琴拧,其原理就相當(dāng)走跟公平鎖類似的邏輯嘱支。

acquire(1);

進(jìn)入acquire方法,該方法是位于AbstractQueuedSynchronizer里沛膳,就是前文提到的AQS汛聚,即抽象同步隊列器倚舀,它相當(dāng)提供一套用戶態(tài)層面的鎖框架,基于它可以實現(xiàn)用戶態(tài)層面的鎖機制话速。

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

注意一點芯侥,NonfairSync和FairSync調(diào)用的acquire(int arg)方法中的tryAcquire方法柱查,其實現(xiàn)是不同的。NonfairSync調(diào)用的acquire方法研乒,其底層tryAcquire調(diào)用的是NonfairSync重寫的tryAcquire方法淋硝;FairSync調(diào)用的acquire方法,其底層tryAcquire調(diào)用的是FairSync重寫的tryAcquire方法竿报。

image

NonfairSync類的acquire方法的流程圖如下??

image

先分析非公平鎖的!tryAcquire(arg)底層源碼實現(xiàn)烈菌,該方法的整體邏輯是芽世,通過getState()獲取state狀態(tài)值诡壁,判斷是否已為0。若state等于0了旺矾,說明此時鎖資源處于無鎖狀態(tài)纽帖,那么懊直,當(dāng)前線程就可以直接再執(zhí)行一遍CAS原子搶鎖操作,若CAS成功雕崩,說明已成功搶占鎖融撞。若state不為0,再判斷當(dāng)前線程是否與占有資源的鎖為同一個線程饶火,若同一個線程,那么就進(jìn)行重入鎖操作当辐,即ReentrantLock支持同一個線程對資源的重復(fù)加鎖鲤看,每次加鎖义桂,就對state值加1,解鎖時袖裕,就對state解鎖罢浇,直至減到0最后釋放鎖。

??最后攒岛,若在該方法里灾锯,通過CAS搶占鎖成功或者重入鎖成功嗅榕,那么就會返回true,若失敗兼雄,就會返回false帽蝶。

final boolean nonfairTryAcquire(int acquires) {
    //獲取當(dāng)前線程引用
    final Thread current = Thread.currentThread();
    //獲取AQS的state狀態(tài)值
    int c = getState();
    //若state等于0了励稳,說明鎖處于無被占用狀態(tài),可被當(dāng)前線程搶占
    if (c == 0) {
        //再次嘗試通過CAS搶鎖
        if (compareAndSetState(0, acquires)) {
            //將獨占線程成員變量設(shè)置為當(dāng)前線程趣避,表示占有鎖資源的線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判斷當(dāng)前線程是否與占有鎖資源的線程為同一個線程
    else if (current == getExclusiveOwnerThread()) {
      //每次重入鎖程帕,state就會加1  
      int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

在 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代碼當(dāng)中,根據(jù) &&短路機制澎羞,若!tryAcquire(arg)為false敛苇,就不會再執(zhí)行后面代碼枫攀。反之株茶,若!tryAcquire(arg)為true,說明搶占鎖失敗了或者不屬于重入鎖蹦掐,那么就會繼續(xù)后續(xù)acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代碼的執(zhí)行僵闯。acquireQueued里面的邏輯鳖粟,就可以理解成“獲取不到鎖,再進(jìn)入隊列排隊順序等待獲取鎖”泳秀。這塊內(nèi)容涉及比較復(fù)雜的雙向鏈表邏輯榄攀,我后面會另外寫一篇文章深入分析檩赢,本文主要是講解公平鎖和非公平鎖的區(qū)別科普。

FairSync公平鎖lock方法里acquire(1)的邏輯與非公平鎖NonfairSync的acquire(1)很類似币他,其底層實現(xiàn)同樣是這樣??

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

我們來看下FairSync類里重實現(xiàn)的tryAcquire與NonfairSync最終執(zhí)行的tryAcquire區(qū)別??

image

可以看到蝴悉,公平鎖FairSync的tryAcquire方法比NonfairSync的nonfairTryAcquire方法多了一行!hasQueuedPredecessors()代碼瘾敢。

在FairSync公平鎖里,若hasQueuedPredecessors()返回false射众,那么!hasQueuedPredecessors()就會為true晃财,在執(zhí)行以下判斷時,就會通過compareAndSetState(0, acquires)即CAS原子搶占鎖罗洗。

if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}

那么伙菜,什么情況下命迈,hasQueuedPredecessors() 能得到false值呢壶愤?

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

?存在兩種情況:

1?? 第一種情況,h != t為false踊淳,說明head和tail節(jié)點都為null或者h(yuǎn)和t都指向一個假節(jié)點head陕靠,這兩種情況都說明了剪芥,此時的同步隊列還沒有初始化,簡單點理解溉躲,就是在當(dāng)前線程之前益兄,還沒有出現(xiàn)線程去搶占鎖净捅,因此,此時荆永,鎖是空閑的, 同時當(dāng)前線程算上最早到來的線程之一(高并發(fā)場景下同一時刻可能存在N個線程同時到來)豆村,就可以通過CAS競爭鎖骂删。

2?? 第二種情況桃漾,h != t為true但(s = h.next) == null || s.thread != Thread.currentThread()為false,當(dāng)頭節(jié)點head和尾節(jié)點都不為空且指向不是同一節(jié)點,就說明同步隊列已經(jīng)初始化恋追,此時至少存在兩個以上節(jié)點罚屋,那么head.next節(jié)點必定不為空脾猛,即(s = h.next) == null會為false,若s.thread != Thread.currentThread()為false羹铅,說明假節(jié)點head的next節(jié)點剛好與當(dāng)前線程為同一節(jié)點愉昆,也就意味著跛溉,當(dāng)前線程排在隊列最前面,排在前面的可以在鎖空閑時獲取鎖資源专肪,就可以執(zhí)行compareAndSetState(0, acquires)去搶占鎖資源堪侯。

若同步隊列已經(jīng)初始化抖格,且當(dāng)前線程又不是在假節(jié)點head的next節(jié)點咕晋,就只能老老實實去后面排隊等待獲取鎖了收奔。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掌呜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坪哄,更是在濱河造成了極大的恐慌质蕉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翩肌,死亡現(xiàn)場離奇詭異模暗,居然都是意外死亡,警方通過查閱死者的電腦和手機念祭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粱坤,“玉大人隶糕,你說我怎么就攤上這事≌拘” “怎么了枚驻?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長株旷。 經(jīng)常有香客問我再登,道長,這世上最難降的妖魔是什么晾剖? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任锉矢,我火速辦了婚禮,結(jié)果婚禮上钞瀑,老公的妹妹穿的比我還像新娘沈撞。我一直安慰自己,他們只是感情好雕什,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布缠俺。 她就那樣靜靜地躺著,像睡著了一般贷岸。 火紅的嫁衣襯著肌膚如雪壹士。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天偿警,我揣著相機與錄音躏救,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛盒使,可吹牛的內(nèi)容都是我干的崩掘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼少办,長吁一口氣:“原來是場噩夢啊……” “哼苞慢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起英妓,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挽放,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蔓纠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辑畦,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年腿倚,在試婚紗的時候發(fā)現(xiàn)自己被綠了纯出。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡敷燎,死狀恐怖潦刃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情懈叹,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布分扎,位于F島的核電站澄成,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏畏吓。R本人自食惡果不足惜墨状,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望菲饼。 院中可真熱鬧肾砂,春花似錦、人聲如沸宏悦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饼煞。三九已至源葫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間砖瞧,已是汗流浹背息堂。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荣堰。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓床未,卻偏偏與公主長得像,于是被迫代替她去往敵國和親振坚。 傳聞我的和親對象是個殘疾皇子薇搁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

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