前言
上一篇文章 我們逐行分析了獨佔鎖的獲取操作, 本篇文章我們來看看獨佔鎖的釋放。如果前面的鎖的獲取流程你已經趟過一遍了, 那鎖的釋放部分就很簡單了, 這篇文章我們直接開始看原始碼.
開始之前先提一句, JAVA的內建鎖在退出臨界區之後是會自動釋放鎖的, 但是ReentrantLock這樣的顯式鎖是需要自己顯式的釋放的, 所以在加鎖之後一定不要忘記在finally塊中進行顯式的鎖釋放:
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新物件
//捕獲異常
} finally {
lock.unlock();
}
一定要記得在 finally
塊中釋放鎖! ! !
一定要記得在 finally
塊中釋放鎖! ! !
一定要記得在 finally
塊中釋放鎖! ! !
Example: ReentrantLock的鎖釋放
由於鎖的釋放操作對於公平鎖和非公平鎖都是一樣的, 所以, unlock
的邏輯並沒有放在 FairSync
或 NonfairSync
裡面, 而是直接定義在 ReentrantLock
類中:
public void unlock() {
sync.release(1);
}
由於釋放鎖的邏輯很簡單, 這裡就不畫流程圖了, 我們直接看原始碼:
release
release方法定義在AQS類中,描述了釋放鎖的流程
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可以看出, 相比獲取鎖的acquire
方法, 釋放鎖的過程要簡單很多, 它只涉及到兩個子函式的呼叫:
-
tryRelease(arg)
- 該方法由繼承AQS的子類實現, 為釋放鎖的具體邏輯
-
unparkSuccessor(h)
- 喚醒後繼執行緒
下面我們分別分析這兩個子函式
tryRelease
tryRelease
方法由ReentrantLock的靜態類Sync
實現:
多嘴提醒一下, 能執行到釋放鎖的執行緒, 一定是已經獲取了鎖的執行緒(這不廢話嘛!)
另外, 相比獲取鎖的操作, 這裡並沒有使用任何CAS操作, 也是因為當前執行緒已經持有了鎖, 所以可以直接安全的操作, 不會產生競爭.
protected final boolean tryRelease(int releases) {
// 首先將當前持有鎖的執行緒個數減1(回溯到呼叫源頭sync.release(1)可知, releases的值為1)
// 這裡的操作主要是針對可重入鎖的情況下, c可能大於1
int c = getState() - releases;
// 釋放鎖的執行緒當前必須是持有鎖的執行緒
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 如果c為0了, 說明鎖已經完全釋放了
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
是不是很簡單? 程式碼都是自解釋的, LZ就不多嘴了.
unparkSuccessor
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
鎖成功釋放之後, 接下來就是喚醒後繼節點了, 這個方法同樣定義在AQS中.
值得注意的是, 在成功釋放鎖之後(tryRelease
返回 true
之後), 喚醒後繼節點只是一個 “附加操作”, 無論該操作結果怎樣, 最後 release
操作都會返回 true
.
事實上, unparkSuccessor 函式也不會返回任何值
接下來我們就看看unparkSuccessor的原始碼:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head節點的ws比0小, 則直接將它設為0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 通常情況下, 要喚醒的節點就是自己的後繼節點
// 如果後繼節點存在且也在等待鎖, 那就直接喚醒它
// 但是有可能存在 後繼節點取消等待鎖 的情況
// 此時從尾節點開始向前找起, 直到找到距離head節點最近的ws<=0的節點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; // 注意! 這裡找到了之並有return, 而是繼續向前找
}
// 如果找到了還在等待鎖的節點,則喚醒它
if (s != null)
LockSupport.unpark(s.thread);
}
在上一篇文章分析 shouldParkAfterFailedAcquire
方法的時候, 我們重點提到了當前節點的前驅節點的 waitStatus
屬性, 該屬性決定了我們是否要掛起當前執行緒, 並且我們知道, 如果一個執行緒被掛起了, 它的前驅節點的 waitStatus
值必然是Node.SIGNAL
.
在喚醒後繼節點的操作中, 我們也需要依賴於節點的waitStatus
值.
下面我們仔細分析 unparkSuccessor
函式:
首先, 傳入該函式的引數node就是頭節點head, 並且條件是
h != null && h.waitStatus != 0
h!=null
我們容易理解, h.waitStatus != 0
是個什麼意思呢?
我不妨逆向來思考一下, waitStatus在什麼條件下等於0? 從上一篇文章到現在, 我們發現之前給 waitStatus賦值過的地方只有一處, 那就是shouldParkAfterFailedAcquire
函式中將前驅節點的 waitStatus
設為Node.SIGNAL
, 除此之外, 就沒有了.
然而, 真的沒有了嗎???
其實還有一處, 那就是新建一個節點的時候, 在addWaiter
函式中, 當我們將一個新的節點新增進佇列或者初始化空佇列的時候, 都會新建節點 而新建的節點的waitStatus
在沒有賦值的情況下都會初始化為0.
所以當一個head節點的waitStatus
為0說明什麼呢, 說明這個head節點後面沒有在掛起等待中的後繼節點了(如果有的話, head的ws就會被後繼節點設為Node.SIGNAL
了), 自然也就不要執行 unparkSuccessor
操作了.
另外一個有趣的問題是, 為什麼要從尾節點開始逆向查詢, 而不是直接從head節點往後正向查詢, 這樣只要正向找到第一個, 不就可以停止查詢了嗎?
首先我們要看到,從後往前找是基於一定條件的:
if (s == null || s.waitStatus > 0)
即後繼節點不存在,或者後繼節點取消了排隊,這一條件大多數條件下是不滿足的。因為雖然後繼節點取消排隊很正常,但是通過上一篇我們介紹的shouldParkAfterFailedAcquire方法可知,節點在掛起前,都會給自己找一個waitStatus狀態為SIGNAL的前驅節點,而跳過那些已經cancel掉的節點。
所以,這個從後往前找的目的其實是為了照顧剛剛加入到佇列中的節點,這就牽涉到我們上一篇特別介紹的“尾分叉”了:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //將當前執行緒包裝成Node
Node pred = tail;
// 如果佇列不為空, 則用CAS方式將當前節點設為尾節點
if (pred != null) {
node.prev = pred; //step 1, 設定前驅節點
if (compareAndSetTail(pred, node)) { // step2, 將當前節點設定成新的尾節點
pred.next = node; // step 3, 將前驅節點的next屬性指向自己
return node;
}
}
enq(node);
return node;
}
如果你仔細看上面這段程式碼, 可以發現節點入隊不是一個原子操作, 雖然用了compareAndSetTail
操作保證了當前節點被設定成尾節點,但是隻能保證,此時step1和step2是執行完成的,有可能在step3還沒有來的及執行到的時候,我們的unparkSuccessor方法就開始執行了,此時pred.next的值還沒有被設定成node,所以從前往後遍歷的話是遍歷不到尾節點的,但是因為尾節點此時已經設定完成,node.prev = pred
操作也被執行過了,也就是說,如果從後往前遍歷的話,新加的尾節點就可以遍歷到了,並且可以通過它一直往前找。
所以總結來說,之所以從後往前遍歷是因為,我們是處於多執行緒併發的條件下的,如果一個節點的next屬性為null, 並不能保證它就是尾節點(可能是因為新加的尾節點還沒來得及執行pred.next = node
), 但是一個節點如果能入隊, 則它的prev屬性一定是有值的,所以反向查詢一定是最精確的。
最後, 在呼叫了 LockSupport.unpark(s.thread)
也就是喚醒了執行緒之後, 會發生什麼呢?
當然是回到最初的原點啦, 從哪裡跌倒(被掛起)就從哪裡站起來(喚醒)唄:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 喏, 就是在這裡被掛起了, 喚醒之後就能繼續往下執行了
return Thread.interrupted();
}
那接下來做什麼呢?
還記得我們上一篇在講“鎖的獲取”的時候留的問題嗎? 如果執行緒從這裡喚醒了,它將接著往下執行。
注意,這裡有兩個執行緒:
一個是我們這篇講的執行緒,它正在釋放鎖,並呼叫了LockSupport.unpark(s.thread)
喚醒了另外一個執行緒;
而這個另外一個執行緒
,就是我們上一節講的因為搶鎖失敗而被阻塞在LockSupport.park(this)
處的執行緒。
我們再倒回上一篇結束的地方,看看這個被阻塞的執行緒被喚醒後,會發生什麼。從上面的程式碼可以看出,他將呼叫 Thread.interrupted()
並返回。
我們知道,Thread.interrupted()
這個函式將返回當前正在執行的執行緒的中斷狀態,並清除它。接著,我們再返回到parkAndCheckInterrupt
被呼叫的地方:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 我們在這裡!在這裡!!在這裡!!!
// 我們在這裡!在這裡!!在這裡!!!
// 我們在這裡!在這裡!!在這裡!!!
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
具體來說,就是這個if語句
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
可見,如果Thread.interrupted()
返回true
,則 parkAndCheckInterrupt()
就返回true, if條件成立,interrupted
狀態將設為true
;
如果Thread.interrupted()
返回false
, 則 interrupted
仍為false
。
再接下來我們又回到了for (;;)
死迴圈的開頭,進行新一輪的搶鎖。
假設這次我們搶到了,我們將從 return interrupted
處返回,返回到哪裡呢? 當然是acquireQueued
的呼叫處啦:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我們看到,如果acquireQueued
的返回值為true
, 我們將執行 selfInterrupt()
:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
而它的作用,就是中斷當前執行緒。
繞了這麼一大圈,到最後還是中斷了當前執行緒,到底是在幹嘛呢?
其實這一切的原因都在於:
我們並不知道執行緒被喚醒的原因。
具體來說,當我們從LockSupport.park(this)
處被喚醒,我們並不知道是因為什麼原因被喚醒,可能是因為別的執行緒釋放了鎖,呼叫了 LockSupport.unpark(s.thread)
,也有可能是因為當前執行緒在等待中被中斷了,因此我們通過Thread.interrupted()
方法檢查了當前執行緒的中斷標誌,並將它記錄下來,在我們最後返回acquire
方法後,如果發現當前執行緒曾經被中斷過,那我們就把當前執行緒再中斷一次。
為什麼要這麼做呢?
從上面的程式碼中我們知道,即使執行緒在等待資源的過程中被中斷喚醒,它還是會不依不饒的再搶鎖,直到它搶到鎖為止。也就是說,它是不響應這個中斷的,僅僅是記錄下自己被人中斷過。
最後,當它搶到鎖返回了,如果它發現自己曾經被中斷過,它就再中斷自己一次,將這個中斷補上。
注意,中斷對執行緒來說只是一個建議,一個執行緒被中斷只是其中斷狀態被設為true
, 執行緒可以選擇忽略這個中斷,中斷一個執行緒並不會影響執行緒的執行。
執行緒中斷是一個很重要的概念,這個我們以後有機會再細講。(已成文,參見Thread類原始碼解讀(3)——執行緒中斷interrupt)
最後再小小的插一句,事實上在我們從return interrupted;
處返回時並不是直接返回的,因為還有一個finally程式碼塊:
finally {
if (failed)
cancelAcquire(node);
}
它做了一些善後工作,但是條件是failed為true,而從前面的分析中我們知道,要從for(;;)中跳出來,只有一種可能,那就是當前執行緒已經拿到了鎖,因為整個爭鎖過程我們都是不響應中斷的,所以不可能有異常丟擲,既然是拿到了鎖,failed就一定是true,所以這個finally塊在這裡實際上並沒有什麼用,它是為響應中斷式的搶鎖所服務的,這一點我們以後有機會再講。
(完)
檢視更多系列文章:系列文章目錄