天天看點

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

本文目錄

    • 前言
    • ReentrantLock定義
    • 鎖的可重入性
    • 什麼是AQS
    • 公平、非公平鎖差別一(lock方法)
    • 核心 AQS 解讀
    • AQS(tryAcquire)嘗試去競争鎖
    • AQS(addWaiter)維護雙向連結清單
    • AQS (acquireQueued)休眠第二個節點後的所有節點
    • 小結 AQS (acquireQueued)作用
    • 公平、非公平鎖差別二(隊列)
    • 釋放鎖
    • ReentrantLock(tryRelease(arg))消除重入次數
    • ReentrantLock(unparkSuccessor) 尾節點掃描喚醒休眠線程
    • 面試專欄

前言

更新點:結合自己閱讀源碼的經驗,新增面試專欄,後續會一直更新。2023-02-16,如果回答的造成了誤解,望斧正。看到并采納會及時修正。

由于疫情,加上忙于工作的原因,也是有段時間沒有寫部落格了,本文是基于以前寫過的部落格再整理出來的,本着加深了解的原則,發現以前的很多文章重新去溫習的時候,讀起來有點晦澀,于是萌生了再整理的想法,同時加了一個面試專欄,讓大家各取所需

首先簡單介紹幾個概念

  • 重量級鎖:使用者起了幾個線程,經過os排程,然後在交給java虛拟機執行。重量級鎖是操作os函數來解決線程同步問題的,涉及到了核心态與使用者态之間的切換,這個開銷是很大的,是以被稱為重量級鎖。
  • 輕量級鎖:由于重量級鎖對os函數的頻繁操作十分耗時,是以衍伸出來了輕量級鎖,目的就是為了減少對核心的直接操作,減少一些可以避免的開銷。而輕量級鎖來解決線程同步問題一般都隻涉及到jdk層面,且我們電腦執行代碼是很快的。
  • 偏向鎖:隻要有人過來競争,偏向鎖就會更新。偏向鎖的意義在于,在隻有一個線程運作或者無競争的情況下,減少輕量級鎖帶來的開銷。
  • 可重入鎖:同一個線程内多次擷取同一把鎖,進行lock操作而不會出現死鎖的情況稱為鎖的可重入性
  • 公平鎖:進行加鎖前會進行判斷看自己是否需要排隊,即使自己是第一個進行lock的線程,遵循先來後到的原則
  • 非公平鎖:沒有隊列的判斷邏輯,誰先執行cas,誰就加鎖成功,誰先搶到就是誰的
  • 自旋鎖:一個線程在擷取鎖的時候,另外一個線程已經搶占了鎖,那麼此線程将一直陷入循環等待的狀态,然後一直判斷是否能擷取鎖成功,直到擷取鎖成功,退出循環

當然本文着重介紹ReentrantLock是怎麼實作的。閱讀本文可以收獲如下知識

  1. 什麼是可重入鎖?
    • 同一個線程内多次擷取同一把鎖,進行lock操作而不會出現死鎖的情況稱為鎖的可重入性
  2. ReentrantLock是一把什麼類型的鎖?哪裡可以展現?
    • ReentrantLock是一把輕量級鎖、可重入鎖。可重入鎖展現在同一個線程可以多次對同一把鎖的lock、unlock操作而不會造成死鎖的情況出現
  3. AQS是什麼,AQS與ReentrantLock有什麼關系?AQS核心是什麼?
    • sync就是個AQS,AQS全稱AbstractQueuedSynchronizer,ReentrantLock的加鎖即sync.lock
    • AQS核心:park、自旋、cas
  4. 并發、并行,它們有啥差别?
    • 并發:并發不一定存在競争,指同一個時間段内,線程數量
    • 并行:存在競争,在同一片刻,競争同一個資源
  5. 知道什麼是公平鎖、非公平鎖嗎?
    • 公平鎖:在ReentrantLock中有一個隊列來維護排隊關系,即使鎖被釋放了,即使自己是隊列排隊的第一個,依然會進行判斷自己是否有擷取鎖的資格。即遵循先來後到的規則
    • 非公平鎖:對比公平鎖是把隊列部分給剔除了,誰先搶到鎖誰就進行cas加鎖成功
  6. 講講你對ReentrantLock的了解
    • 在jdk1.6前:Synchronized是通過操作os函數來實作線程間的同步問題的是一把重量級鎖
    • jdk1.6之後ReentrantLock對比Synchronized都差不多,Synchronized底層做了優化,有一個鎖更新的過程,然後就是ReentrantLock的調用方法更加豐富一點
  7. ReentrantLock是怎麼實作的,有了解過嗎?
    • ReentrantLock主要利用AQS實作的,而AQS核心又是park、自旋、cas
  8. ReentrantLock加鎖的大概流程是怎麼樣的?
    • 先嘗試去擷取鎖(先看是否需要排隊,不需要排隊則cas加鎖。如果是同一個線程來操作,重入鎖狀态辨別符++),擷取不到鎖把目前thread封裝成一個Node節點放入隊列中,維護好隊列關系後,如果發現自己的排隊的第一個人,那麼最多還會去嘗試擷取鎖2次,實在擷取不到鎖了,讓目前Node睡眠,最後執行finally中的方法去取消目前線程的競争
  9. ReentrantLock中的隊列什麼情況下會被初始化?
    • 至少存在倆個 線程競争的情況下才會被初始化

ReentrantLock定義

首先 ReentrantLock 是一把可重入鎖、輕量級鎖,至于是公平鎖還是非公平鎖,看我們怎麼把它執行個體化出來的,預設情況下是一把非公平鎖,當我們建立執行個體的時候,傳入參數 true,此時就是一把公平鎖。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

鎖的可重入性

可重入鎖示例代碼展現如下,同個線程倆次擷取同一把鎖并未出現死鎖的情況。

new Thread(() -> {
            int i = 0;
            lock.lock();
            System.out.println("初始化鎖:" + lock);
            while (true) {
                lock.lock();
                System.out.println("第" + ++i + "次拿到鎖" + lock);
                if (i == 100) {
                    break;
                }
                lock.unlock();
            }
            lock.unlock();
            System.out.println(i);
        }).start();
           

什麼是AQS

ReentrantLock 的加鎖本質是利用 sync 中的 lock()方法實作的。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

而 sync 是繼承了一個叫做 AbstractQueuedSynchronizer 的類,這個類也就是我們所說的 AQS(AbstractQueuedSynchronizer)那麼我們要徹底搞懂 ReentrantLock 的加鎖流程,閱讀 AQS 的源碼就好了。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

公平、非公平鎖差別一(lock方法)

公平鎖

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

非公平鎖

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

通過觀察上圖,不難發現不論是非公平、公平鎖裡面都用到了 acquire(1) 方法,唯一的差別就是,非公平鎖遵循先來後到的原則,誰先 CAS 成功,誰就加鎖成功,其他 CAS 失敗的線程最終走 AQS 裡面的那套邏輯。

而大家被面試官問到 ReentrantLock中非公平、公平鎖的差別是什麼的時候?

我們回答: ReentrantLock 中的非公平鎖在加鎖的時候,對第一個有加鎖需求的線程直接

CAS 将 NonfairSync 中的 state 字段值改為了 1,後到的線程由于 CAS 修改 state 預期值不為 0 了,修改失敗都走 AQS 中的 acquire()那套邏輯去了。 這就是 ReentrantLock中的非公平鎖、公平鎖的最淺顯差別。如果你覺得自己夠牛逼,還可以順帶提一嘴:值得一提的是,為了防止工作記憶體中的資料沒有及時同步至主記憶體,設計 state 是被 volatile 關鍵字修飾的 ,寫 state 時工作記憶體立即重新整理主記憶體,讀 state 時直接從主記憶體中讀取。

state 預設值是 0

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘
接下來面試官覺得你有點東西就會問:你剛才說到的 AQS、acquire() 是個什麼東西?可以講講嗎?

核心 AQS 解讀

上文提到了,不管是 ReentrantLock 公平鎖、非公平鎖,最終都用到了 acquire 方法,而裡面其實可以拆分成 tryAcquire、acquireQueued、addWaiter、selfInterrupt 四小闆塊來分析,且 tryAcquire 分公平鎖、非公平鎖倆套邏輯,目的明确了,開始上菜。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

AQS(tryAcquire)嘗試去競争鎖

  1. state 為 0: 鎖已經變為可搶狀态,第一次沒有搶到鎖的線程還會進行一次 CAS 搶鎖的操作
  2. state 為 1 :同一個線程重複加鎖,由于第一個搶到鎖的線程,CAS 成功了,state 變為了 1 ,但是此線程接着加鎖操作,就會來到 tryAcquire 中這,由于占有鎖的線程和正在加鎖的線程是同一個,對 state++ 操作,這就是ReentrantLock 是可重入鎖的原因。
ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘
ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

AQS(addWaiter)維護雙向連結清單

  • 尾節點非空時:多個線程同時沒搶到鎖,都走到了入隊的邏輯,第一個入隊成功,其他入隊失敗的線程就會走 enq(node)裡面的邏輯,是以 enq 方法設定成一個死循環,沒入隊的線程必須要入隊的
ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘
  • 尾節點為空時:假設當所有沒搶到鎖的線程進行入隊的時候,可能同時執行 addWaiter 方法,同時判斷尾節點為 null ,此時這些線程會去走 enq(node)裡面的邏輯 CAS 尾插維護雙向連結清單。還有一種情況就是:當尾節點已經有的時候直接 CAS 尾插入隊,其他尾插入隊失敗的線程接着走 enq(node)裡面的邏輯入隊。值得一提的是:看下圖我們可以得知雙向連結清單的第一個節點是一個空的 node,
    ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

AQS (acquireQueued)休眠第二個節點後的所有節點

值得一提的是代碼片段一中的條件告訴我們:隻有是第二個節點才有資格再次嘗試擷取鎖,其他的節點都會走代碼片段二中的邏輯,裡面會對所有競争失敗且入隊排在大于 2 位置的所有節點進行 park(線程休眠),隻有當占有鎖的線程釋放鎖的時候,休眠的線程才會接着走 for(;;)中的邏輯。劇透一下,ReentrantLock 釋放鎖的邏輯是按照,從連結清單依次從左往右的順序喚醒線程的,這也能解釋為什麼代碼片段三中,隻有是前節點為頭節點的 node 才能嘗試競争鎖的設計了,且如果競争鎖成功後,要設定 setHead(node); ,這個就好比排隊叫号,第一個被叫号的人人走掉了,第二個人是不是就是變成第一個排隊的人了。

代碼片段一

代碼片段二

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
           

代碼片段三

if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
           

如果 node 節點的前一個節點非頭節點,進行 park 休眠操作,直至符合條件的 node 節點繼續嘗試擷取鎖成功,然後 Return 了後執行 finally 中的邏輯,這裡面的代碼沒有過多深究,改天專門寫一篇文章分析 AQS。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

小結 AQS (acquireQueued)作用

acquireQueued 方法會休眠不符合再次競争鎖條件的隊列中的線程,同時設計成一個死循環,等待隊列中被喚醒的線程重新去競争鎖,值得一提的是隻有當該線程的前一個節點符合為頭節點的條件,才能繼續嘗試競争鎖。同時裡面在對線程進行 park 的時候會去修改 node 節點中的一些屬性,例如:修改 waitStatus(修改成waitStatus非0以外的數,隻有 waitStatus != 0 的節點才能被喚醒),重新更新連結清單的操作等。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
           

公平、非公平鎖差別二(隊列)

上文一直都在分析非公平鎖,接下來分析公平鎖源碼,公平鎖與非公平鎖還有一個非常重要的差別就是,公平鎖的 tryAcquire 方法實作上,在進行 CAS 加鎖前,會去判斷隊列中是否存在節點,如果隊列中還有節點,且沒到取号的時候,這個線程是不能去競争鎖的,這也是公平鎖先來後到的一個展現,其他流程和非公平鎖的實作一摸一樣。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

釋放鎖

代碼如下,可以看到釋放鎖成功,需要達到倆個條件

  • tryRelease(arg) 傳回 true
  • 頭節點不為 null 且,waitStatus !=0;
ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

ReentrantLock(tryRelease(arg))消除重入次數

消除重入鎖的次數,隻有當重入的次數都消除後,此方法才會傳回 true,才有機會走下面的 unparkSuccessor 中的邏輯。而 unparkSuccessor 裡面會去喚醒隊列中的線程,讓第一次沒有搶到鎖的線程再次去 tryAcquire 去競争鎖。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

是以大家在使用 ReentrantLock 中的時候 每 lock 一次,一定要對應 unlock 一次,看下圖一,加鎖了2次,隻解鎖一次,那麼這個鎖的還是沒有被解開的,其他線程還處于 park 休眠狀态,根本沒機會被喚醒去競争鎖。

圖一

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

當 lock 與 unlock 方法配套使用的時候,可以看到除了線程 1 的線程被正常喚醒,也可以去競争鎖了。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

ReentrantLock(unparkSuccessor) 尾節點掃描喚醒休眠線程

下圖圈綠色的地方即為 尾節點掃描對應的節點,然後圈紅的地方會去喚醒尾掃描得到節點線程,相信很多人被人問到:

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

被喚醒的線程接着執行目錄:AQS (acquireQueued)休眠第二個節點後的所有節點裡面的死循環邏輯,去 tryAcquire 去再次競争鎖,直至所有線程都加鎖成功。

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

面試專欄

為什麼要尾節點掃描去喚醒線程啊

答:維護連結清單的時候他先是建立了指向前一個節點的引用,在并發很高的時候,可能此時的連結清單引用還沒維護好呢,所有節點都隻有一個前向引用(入下圖三),此時你要去喚醒線程,如果頭節點掃描,壓根就掃描不到節點,因為此時指向後繼節點的引用都還未建立。

圖一

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

圖二

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘

圖三

ReentrantLock源碼探究、探究公平鎖與非公平鎖背後的奧秘
講講你對 ReentrantLock 的了解。(問的比較寬泛,我們也簡答一下,循序漸進的引導式的回答吧,如果面試官很羞澀,且我們自身實力夠硬把從加鎖到解鎖的整個流程都說一遍)

答:ReentrantLock 是基于 AQS 實作的一把可重入鎖、根據執行個體化傳參的不同,也分公平鎖、非公平鎖。

能說說你對 ReentrantLock 公平鎖、非公平鎖的了解嗎?

ReentrantLock 非公平鎖在 lock 的時候不管先來後到直接 CAS 去修改 state 值為 1,其他修改失敗的線程,會進行入隊列,然後 park 休眠,等待被喚醒然後重新 CAS 去競争鎖。而 ReentrantLock 公平鎖與其的差別就是,公平鎖在 tryAcquire 的時候會去判斷隊列中是否存在 node 節點,有則排隊去加入隊列休眠,然後等待被喚醒再次去 tryAcquire 去競争鎖,而非公平鎖在 tryAcquire 的時候,講究的是一個先來後到,沒有判斷隊列節點的邏輯。

說說你在實際開發中使用 ReentrantLock 遇到的問題?

多線程下 lock 與 unlock 方法沒有配套使用,造成解鎖的時候隻是消除了 重入次數,并沒有真正的去解鎖,導緻 隊列中被 park 的線程沒有被喚醒,導緻許多邏輯沒有正常執行。

🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹如果大家在面試中被問到和 ReentrantLock 的問題,本文看到回複會及時跟進,并同步文章的🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹