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個不那麼重要的工作,
- tryAcquire
- addWaiter
- acquireQueued
- selfInterrupt(不那麼重要的部分)
tryAcquire
返回true則表示無需阻塞,直接運行即可,有兩種可能,
- 當前狀態無鎖,並且沒有執行中的隊列
- 有鎖,但持有者是自己.
返回false代表取鎖失敗,有幾種可能
- 當前有鎖,並且並且鎖不是自己,直接退出,返回
- 當前無鎖,但是有運行中的隊列,公平鎖的情況下,會跳出方法到外層的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
死循環來保證,一定是有頭尾節點後,才返回
邏輯稍微有點繞,簡單的說
- 初始化頭結點,並且傳入的node是一個沒有線程的,new出來的
- 尾=頭
- cas,把尾節點設置成當前節點
- 頭節點的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喚醒
- 判斷當前線程是不是持有鎖的線程,如果不是直接拋異常了
- 設置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括號裏進行大量的工作執行.