系列文章目錄 java
上一篇文章 咱們逐行分析了獨佔鎖的獲取操做, 本篇文章咱們來看看獨佔鎖的釋放。若是前面的鎖的獲取流程你已經趟過一遍了, 那鎖的釋放部分就很簡單了, 這篇文章咱們直接開始看源碼.node
開始以前先提一句, JAVA的內置鎖在退出臨界區以後是會自動釋放鎖的, 可是ReentrantLock這樣的顯式鎖是須要本身顯式的釋放的, 因此在加鎖以後必定不要忘記在finally塊中進行顯式的鎖釋放:segmentfault
Lock lock = new ReentrantLock(); ... lock.lock(); try { // 更新對象 //捕獲異常 } finally { lock.unlock(); }
必定要記得在 finally
塊中釋放鎖! ! !
必定要記得在 finally
塊中釋放鎖! ! !
必定要記得在 finally
塊中釋放鎖! ! !安全
因爲鎖的釋放操做對於公平鎖和非公平鎖都是同樣的, 因此, unlock
的邏輯並無放在 FairSync
或 NonfairSync
裏面, 而是直接定義在 ReentrantLock
類中:多線程
public void unlock() { sync.release(1); }
因爲釋放鎖的邏輯很簡單, 這裏就不畫流程圖了, 咱們直接看源碼:併發
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
方法, 釋放鎖的過程要簡單不少, 它只涉及到兩個子函數的調用:ui
tryRelease(arg)this
unparkSuccessor(h)線程
下面咱們分別分析這兩個子函數
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就很少嘴了.
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塊在這裏實際上並無什麼用,它是爲響應中斷式的搶鎖所服務的,這一點咱們之後有機會再講。
(完)
查看更多系列文章:系列文章目錄