java鎖ReentrantLock的源碼分析

簡單的函數介紹

聲明鎖對象,構造函數默認不傳是創建非公平鎖,傳true是創建一個公平鎖
ReentrantLock lock = new ReentrantLock(true);

重入鎖,lock和unlock成對出現,
lock.lock();
lock.unlock();

嘗試加鎖,有返回值的加鎖成功返回true,失敗則
lock.tryLock();

鎖阻塞期間,可以掉用線程的interrupt打斷,然後用InterruptedException捕獲來中斷鎖阻塞的狀態,
lock.lockInterruptibly();
InterruptedException
myThread1.interrupt();

加鎖過程猜測

瞭解過ReentrantLock裏面有一個AQS(AbstractQueuedSynchronizer),抽象的隊列同步的? 用它來實現的加鎖解鎖,這兩天看了一點源碼,做個筆記.
在看源碼之前,首先猜測aqs應該就是一個隊列,因爲它有公平鎖一說,所以必然隊列是先進先出的,然後應該是當前對象獲取到鎖之後,後續進來的會去隊列裏排隊等待,解鎖之後,取出隊列的第一個進行喚醒.

加鎖流程源碼分析

下面部分直接說結論了,不循序漸進了

首先看公平鎖的實現,非公平鎖相對還要簡單一些.

在這裏插入圖片描述
我看下來,覺得公平鎖的核心部分就是這個函數,實際上這裏進行了3個比較重要的工作和1個不那麼重要的工作,

  1. tryAcquire
  2. addWaiter
  3. acquireQueued
  4. selfInterrupt(不那麼重要的部分)

tryAcquire

在這裏插入圖片描述
返回true則表示無需阻塞,直接運行即可,有兩種可能,

  1. 當前狀態無鎖,並且沒有執行中的隊列
  2. 有鎖,但持有者是自己.

返回false代表取鎖失敗,有幾種可能

  1. 當前有鎖,並且並且鎖不是自己,直接退出,返回
  2. 當前無鎖,但是有運行中的隊列,公平鎖的情況下,會跳出方法到外層的acquireQueued中繼續,哪怕隊列是空的. 非公平鎖沒有這一步判斷,直接拿鎖走人

簡單說下代碼流程

這個函數是獲取鎖的主要部分,而且後續也在其他流程裏多次調用了這個函數,
state是當前鎖狀態, =0代表無鎖,=1代表有鎖
如果無鎖狀態下,會進入隊列判斷,也就是hasQueuedPredecessors

在這裏插入圖片描述
這個地方進行了多次的非判斷,很乾擾閱讀的邏輯,
首先說明,AQS會單獨存儲隊列中的頭尾節點,tail和head.
首先頭尾相等只有一種可能,就是兩個都是null,所以用第一個判斷來確定,如果都是null則沒有正在跑的隊列,
第二個是判斷當前線程是不是head.next. 如果它不是則沒有資格繼續拿鎖,直接返回false,如果是,才進入往下判斷.

compareAndSetState() //更新狀態,不說了,
setExclusiveOwnerThread() //設置當前拿鎖線程,

下面這部分是判斷重入鎖的部分,如果當前線程就是拿到了鎖的線程,則再次進入,並且State+=1

else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

至此,tryAcquire部分結束,主要就是嘗試加鎖,true成功,false失敗,

addWaiter

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

回到acquire,tryAcquire返回值爲true,並且這裏是"非"true的話,也就是說,返回true,就直接整個判斷退出了(這裏有點繞),如果是返回false,則進入下面一個函數,按照執行邏輯,先進入addWaiter.
在這裏插入圖片描述
AQS中的Q是個雙向鏈表,其中的節點就是這個Node,結構爲:waitStatus/prev/next/thread
剛纔說了,會單獨存儲頭尾節點,此時嘗試取出尾結點,原則上新進來的線程需要加到隊列的最尾部分,之前的尾步成爲倒數第二,
如果尾結點==null,則會進入enq進行隊列初始化

enq

在這裏插入圖片描述
死循環來保證,一定是有頭尾節點後,才返回
邏輯稍微有點繞,簡單的說

  1. 初始化頭結點,並且傳入的node是一個沒有線程的,new出來的
  2. 尾=頭
  3. cas,把尾節點設置成當前節點
  4. 頭節點的next指定成當前節點

這有個比較怪的地方,
讀代碼,很容易誤解成 t.next = node 是在把尾節點的next指定成當前節點,但實際上這個t是之前的head節點的引用,真正的尾節點已經在compareAndSetTail中更新成了當前節點.
爲什麼<<尾=頭>>,而且下面還用了unsafe的方法,不能直接用兩個引用分別做事情嗎…

至此addWaiter結束,此時完成了對當前線程的節點封裝,並且保證了隊列當中有頭尾節點.

acquireQueued

把剛纔包裝好的node加到隊列裏,
在這裏插入圖片描述
這裏也是一個死循環,
先是取出當前節點的前一個,判斷前一個是不是頭結點,如果是,則再次調用tryAcquire來嘗試獲取鎖資源,如果此時成功了,會更新節點信息後,直接放回,相當於拿到了鎖,相當於一次自旋,
如果失敗則進入第二個if,

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
           do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上面代碼由於源碼中註釋太多,沒有用截圖,直接把註釋刪掉貼了過來,
有一個需要注意的是,這裏判斷的主體都是以當前節點的前一個,如果前一個是SIGNAL(這理解不是很透徹,直接貼一段註釋)

/** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;

如果是SIGNAL則返回true,當前線程進入park
如果不是SIGNAL,則會進入到下面的else裏,把上一個設置成SIGNAL,待死循環的下一次執行進來直接返回true.

解鎖流程

邏輯猜測:
估計就是取當前隊列的第一個,進行喚醒,應該比加鎖過程簡單的多
直接看碼
在這裏插入圖片描述
上圖是核心部分,“嘗試解鎖”,“嘗試成功”,則取到head,進行unpark喚醒

在這裏插入圖片描述

  1. 判斷當前線程是不是持有鎖的線程,如果不是直接拋異常了
  2. 設置aqs的state爲0,設置持有鎖的線程爲null,相當於釋放了鎖資源

在這裏插入圖片描述
取到head,如果head的waitStatus>0則不需要喚醒任何,<0則設置成0,並且走喚醒邏輯
取到頭的next節點,判斷非空,或者等待狀態>0,正常情況下它應該是-1的,這種是排除隊列中有其他的異常情況,註釋中寫道<<要解鎖的通常是h的下一個節點,但如果出現了明顯的null,或狀態取消,則從尾部開始向前便利.>>
一個for循環,不斷從後向前便利,改變目標節點的值,最終取到的目標節點就是,在隊列裏面第一個不爲空且狀態是對的,
拿到節點,進行unpark

tryLock,和lockInterruptibly

在這裏插入圖片描述
tryLock,直接調用了非公平鎖的,請求鎖方法tryAcquire,並且直接把tryAcquire的返回值返回了.相當於沒有取到鎖的話,沒有後續的排隊環節,直接返回false了.

在這裏插入圖片描述
lockInterruptibly,等待鎖的過程中,可以調用線程的Interrupt進行打斷,然後用一個異常捕捉到,然後讓線程繼續做別的事情.核心在於這個地方直接拋出了異常,

總結

一會會畫一個流程圖,走一遍流程,
代碼裏有兩個我不太喜歡的地方
1:有太多的雙非判斷,不好理解,
2:執行邏輯和判斷邏輯是結合在一起的,我一般會盡量的把執行和判斷分開,哪怕代碼執行效率會略微下降一點,儘量避免在if括號裏進行大量的工作執行.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章