摘要:我們知道,這個函數將返回當前正在執行的線程的中斷狀態,并清除它。注意,中斷對線程來說只是一個建議,一個線程被中斷只是其中斷狀態被設為線程可以選擇忽略這個中斷,中斷一個線程并不會影響線程的執行。
前言
系列文章目錄
上一篇文章 我們逐行分析了獨占鎖的獲取操作, 本篇文章我們來看看獨占鎖的釋放。如果前面的鎖的獲取流程你已經趟過一遍了, 那鎖的釋放部分就很簡單了, 這篇文章我們直接開始看源碼.
開始之前先提一句, JAVA的內置鎖在退出臨界區之后是會自動釋放鎖的, 但是ReentrantLock這樣的顯式鎖是需要自己顯式的釋放的, 所以在加鎖之后一定不要忘記在finally塊中進行顯式的鎖釋放:
Lock lock = new ReentrantLock(); ... lock.lock(); try { // 更新對象 //捕獲異常 } finally { lock.unlock(); }
一定要記得在 finally 塊中釋放鎖! ! !
一定要記得在 finally 塊中釋放鎖! ! !
一定要記得在 finally 塊中釋放鎖! ! !
由于鎖的釋放操作對于公平鎖和非公平鎖都是一樣的, 所以, unlock的邏輯并沒有放在 FairSync 或 NonfairSync 里面, 而是直接定義在 ReentrantLock類中:
public void unlock() { sync.release(1); }
由于釋放鎖的邏輯很簡單, 這里就不畫流程圖了, 我們直接看源碼:
releaserelease方法定義在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)
喚醒后繼線程
下面我們分別分析這兩個子函數
tryReleasetryRelease方法由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就不多嘴了.
unparkSuccessorpublic 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塊在這里實際上并沒有什么用,它是為響應中斷式的搶鎖所服務的,這一點我們以后有機會再講。
(完)
查看更多系列文章:系列文章目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76474.html
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學習經歷。因為寫作的時候發現,為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
摘要:而對于共享鎖而言,由于鎖是可以被共享的,因此它可以被多個線程同時持有。換句話說,如果一個線程成功獲取了共享鎖,那么其他等待在這個共享鎖上的線程就也可以嘗試去獲取鎖,并且極有可能獲取成功。 前言 前面兩篇我們以ReentrantLock為例了解了AQS獨占鎖的獲取與釋放,本篇我們來看看共享鎖。由于AQS對于共享鎖與獨占鎖的實現框架比較類似,因此如果你搞定了前面的獨占鎖模式,則共享鎖也就很...
摘要:本篇我們將以的公平鎖為例來詳細看看使用獲取獨占鎖的流程。本文中的源碼基于。由于本篇我們分析的是獨占鎖,同一時刻,鎖只能被一個線程所持有。由于在整個搶鎖過程中,我們都是不響應中斷的。 前言 AQS(AbstractQueuedSynchronizer)是JAVA中眾多鎖以及并發工具的基礎,其底層采用樂觀鎖,大量使用了CAS操作, 并且在沖突時,采用自旋方式重試,以實現輕量級和高效地獲取鎖...
摘要:前言本篇文章是基于線程間的同步與通信和這篇文章寫的,在那篇文章中,我們分析了接口所定義的方法,本篇我們就來看看對于接口的這些接口方法的具體實現。因此,條件隊列在出隊時,線程并不持有鎖。 前言 本篇文章是基于線程間的同步與通信(4)——Lock 和 Condtion 這篇文章寫的,在那篇文章中,我們分析了Condition接口所定義的方法,本篇我們就來看看AQS對于Condition接口...
閱讀 1837·2023-04-25 14:49
閱讀 3117·2021-09-30 09:47
閱讀 3100·2021-09-06 15:00
閱讀 2224·2019-08-30 13:16
閱讀 1436·2019-08-30 10:48
閱讀 2668·2019-08-29 15:11
閱讀 1287·2019-08-26 14:06
閱讀 1663·2019-08-26 13:30