摘要:從上面的代碼可以看出,條件隊列是建立在鎖基礎上的,而且必須是獨占鎖原因后面會通過源碼分析。明天就是國慶長假了,我自己也計劃出國玩一趟,散散心。提前祝廣大朋友國慶快樂。
相比于獨占鎖跟共享鎖,AbstractQueuedSynchronizer中的條件隊列可能被關注的并不是很多,但它在阻塞隊列的實現里起著至關重要的作用,同時如果想全面了解AQS,條件隊列也是必須要學習的。
原文地址:http://www.jianshu.com/p/3f8b...
這篇文章會涉及到AQS中獨占鎖跟共享鎖的一些知識,如果你已經對這兩塊內容很了解了,那就直接往下看。否則在讀本文之前還是建議讀者先去看看我之前寫的兩篇文章溫習一下。
深入淺出AQS之獨占鎖模式
深入淺出AQS之共享鎖模式
區別于前面兩篇文章,可能之前很多人都沒有太在意AQS中的這塊內容,所以這篇文章我們先來看下條件隊列的使用場景:
//首先創建一個可重入鎖,它本質是獨占鎖 private final ReentrantLock takeLock = new ReentrantLock(); //創建該鎖上的條件隊列 private final Condition notEmpty = takeLock.newCondition(); //使用過程 public E take() throws InterruptedException { //首先進行加鎖 takeLock.lockInterruptibly(); try { //如果隊列是空的,則進行等待 notEmpty.await(); //取元素的操作... //如果有剩余,則喚醒等待元素的線程 notEmpty.signal(); } finally { //釋放鎖 takeLock.unlock(); } //取完元素以后喚醒等待放入元素的線程 }
上面的代碼片段截取自LinkedBlockingQueue,是Java常用的阻塞隊列之一。
從上面的代碼可以看出,條件隊列是建立在鎖基礎上的,而且必須是獨占鎖(原因后面會通過源碼分析)。
等待條件的過程:
在操作條件隊列之前首先需要成功獲取獨占鎖,不然直接在獲取獨占鎖的時候已經被掛起了。
成功獲取獨占鎖以后,如果當前條件還不滿足,則在當前鎖的條件隊列上掛起,與此同時釋放掉當前獲取的鎖資源。這里可以考慮一下如果不釋放鎖資源會發生什么?
如果被喚醒,則檢查是否可以獲取獨占鎖,否則繼續掛起。
條件滿足后的喚醒過程(以喚醒一個節點為例,也可以喚醒多個):
把當前等待隊列中的第一個有效節點(如果被取消就無效了)加入同步隊列等待被前置節點喚醒,如果此時前置節點被取消,則直接喚醒該節點讓它重新在同步隊列里適當的嘗試獲取鎖或者掛起。
注:說到這里必須要解釋一個知識點,整個AQS分為兩個隊列,一個同步隊列,一個條件隊列。只有同步隊列中的節點才能獲取鎖。前面兩篇獨占鎖共享鎖文章中提到的加入隊列就是同步隊列。條件隊列中所謂的喚醒是把節點從條件隊列移到同步隊列,讓節點有機會去獲取鎖。
二、源碼深入分析下面的代碼稍微復雜一點,因為它考慮了中斷的處理情況。我由于想跟文章開頭的代碼片段保持一致,所以選取了該方法進行說明。如果只想看核心邏輯的話,那推薦讀者看看awaitUninterruptibly()方法的源碼。
//條件隊列入口,參考上面的代碼片段 public final void await() throws InterruptedException { //如果當前線程被中斷則直接拋出異常 if (Thread.interrupted()) throw new InterruptedException(); //把當前節點加入條件隊列 Node node = addConditionWaiter(); //釋放掉已經獲取的獨占鎖資源 int savedState = fullyRelease(node); int interruptMode = 0; //如果不在同步隊列中則不斷掛起 while (!isOnSyncQueue(node)) { LockSupport.park(this); //中斷處理,另一種跳出循環的方式 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //走到這里說明節點已經條件滿足被加入到了同步隊列中或者中斷了 //這個方法很熟悉吧?就跟獨占鎖調用同樣的獲取鎖方法,從這里可以看出條件隊列只能用于獨占鎖 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; //走到這里說明已經成功獲取到了獨占鎖,接下來就做些收尾工作 //刪除條件隊列中被取消的節點 if (node.nextWaiter != null) unlinkCancelledWaiters(); //根據不同模式處理中斷 if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
流程比較復雜,一步一步來分析,首先看下加入條件隊列的代碼:
//注:1.與同步隊列不同,條件隊列頭尾指針是firstWaiter跟lastWaiter //注:2.條件隊列是在獲取鎖之后,也就是臨界區進行操作,因此很多地方不用考慮并發 private Node addConditionWaiter() { Node t = lastWaiter; //如果最后一個節點被取消,則刪除隊列中被取消的節點 //至于為啥是最后一個節點后面會分析 if (t != null && t.waitStatus != Node.CONDITION) { //刪除所有被取消的節點 unlinkCancelledWaiters(); t = lastWaiter; } //創建一個類型為CONDITION的節點并加入隊列,由于在臨界區,所以這里不用并發控制 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } //刪除取消節點的邏輯雖然長,但比較簡單,就不多帶帶說了,就是鏈表刪除 private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
把節點加入到條件隊列中以后,接下來要做的就是釋放鎖資源:
//入參就是新創建的節點,即當前節點 final int fullyRelease(Node node) { boolean failed = true; try { //這里這個取值要注意,獲取當前的state并釋放,這從另一個角度說明必須是獨占鎖 //可以考慮下這個邏輯放在共享鎖下面會發生什么? int savedState = getState(); //跟獨占鎖釋放鎖資源一樣,不贅述 if (release(savedState)) { failed = false; return savedState; } else { //如果這里釋放失敗,則拋出異常 throw new IllegalMonitorStateException(); } } finally { //如果釋放鎖失敗,則把節點取消,由這里就能看出來上面添加節點的邏輯中只需要判斷最后一個節點是否被取消就可以了 if (failed) node.waitStatus = Node.CANCELLED; } }
走到這一步,節點也加入條件隊列中了,鎖資源也釋放了,接下來就該掛起了(先忽略中斷處理,單看掛起邏輯):
//如果不在同步隊列就繼續掛起(signal操作會把節點加入同步隊列) while (!isOnSyncQueue(node)) { LockSupport.park(this); //中斷處理后面再分析 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //判斷節點是否在同步隊列中 final boolean isOnSyncQueue(Node node) { //快速判斷1:節點狀態或者節點沒有前置節點 //注:同步隊列是有頭節點的,而條件隊列沒有 if (node.waitStatus == Node.CONDITION || node.prev == null) return false; //快速判斷2:next字段只有同步隊列才會使用,條件隊列中使用的是nextWaiter字段 if (node.next != null) return true; //上面如果無法判斷則進入復雜判斷 return findNodeFromTail(node); } //注意這里用的是tail,這是因為條件隊列中的節點是被加入到同步隊列尾部,這樣查找更快 //從同步隊列尾節點開始向前查找當前節點,如果找到則說明在,否則不在 private boolean findNodeFromTail(Node node) { Node t = tail; for (;;) { if (t == node) return true; if (t == null) return false; t = t.prev; } }
如果被喚醒且已經被轉移到了同步隊列,則會執行與獨占鎖一樣的方法acquireQueued()進行同步隊列獨占獲取。
最后我們來梳理一下里面的中斷邏輯以及收尾工作的代碼:
while (!isOnSyncQueue(node)) { LockSupport.park(this); //這里被喚醒可能是正常的signal操作也可能是中斷 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //這里的判斷邏輯是: //1.如果現在不是中斷的,即正常被signal喚醒則返回0 //2.如果節點由中斷加入同步隊列則返回THROW_IE,由signal加入同步隊列則返回REINTERRUPT private int checkInterruptWhileWaiting(Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; } //修改節點狀態并加入同步隊列 //該方法返回true表示節點由中斷加入同步隊列,返回false表示由signal加入同步隊列 final boolean transferAfterCancelledWait(Node node) { //這里設置節點狀態為0,如果成功則加入同步隊列 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { //與獨占鎖同樣的加入隊列邏輯,不贅述 enq(node); return true; } //如果上面設置失敗,說明節點已經被signal喚醒,由于signal操作會將節點加入同步隊列,我們只需自旋等待即可 while (!isOnSyncQueue(node)) Thread.yield(); return false; }
在把喚醒后的中斷判斷做好以后,看await()中最后一段邏輯:
//在處理中斷之前首先要做的是從同步隊列中成功獲取鎖資源 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; //由于當前節點可能是由于中斷修改了節點狀態,所以如果有后繼節點則執行刪除已取消節點的操作 //如果沒有后繼節點,根據上面的分析在后繼節點加入的時候會進行刪除 if (node.nextWaiter != null) unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); //根據中斷時機選擇拋出異常或者設置線程中斷狀態 private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) //實現代碼為:Thread.currentThread().interrupt(); selfInterrupt(); }
至此條件隊列await操作全部分析完畢。signal()方法相對容易一些,一起看源碼分析下:
//條件隊列喚醒入口 public final void signal() { //如果不是獨占鎖則拋出異常,再次說明條件隊列只適用于獨占鎖 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); //如果條件隊列不為空,則進行喚醒操作 Node first = firstWaiter; if (first != null) doSignal(first); } //該方法就是把一個有效節點從條件隊列中刪除并加入同步隊列 //如果失敗則會查找條件隊列上等待的下一個節點直到隊列為空 private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) &&(first = firstWaiter) != null); } //將節點加入同步隊列 final boolean transferForSignal(Node node) { //修改節點狀態,這里如果修改失敗只有一種可能就是該節點被取消,具體看上面await過程分析 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //該方法很熟悉了,跟獨占鎖入隊方法一樣,不贅述 Node p = enq(node); //注:這里的p節點是當前節點的前置節點 int ws = p.waitStatus; //如果前置節點被取消或者修改狀態失敗則直接喚醒當前節點 //此時當前節點已經處于同步隊列中,喚醒會進行鎖獲取或者正確的掛起操作 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }三、總結
相比于獨占鎖跟共享鎖,條件隊列可能是最不受關注的了,但由于它是阻塞隊列實現的關鍵組件,還是有必要了解一下其中的原理。其實我認為關鍵點有兩條,第一是條件隊列是建立在某個具體的鎖上面的,第二是條件隊列跟同步隊列是兩個隊列,前者依賴條件喚醒后者依賴鎖釋放喚醒,了解了這兩點以后搞清楚條件隊列就不是什么難事了。
至此,Java同步器AQS中三大鎖模式就都分析完了。雖然已經盡力思考,盡量寫的清楚,但鑒于水平有限,如果有紕漏的地方,歡迎廣大讀者指正。
明天就是國慶長假了,我自己也計劃出國玩一趟,散散心。
提前祝廣大朋友國慶快樂。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67653.html
摘要:原文地址深入淺出之獨占鎖模式深入淺出之共享鎖模式深入淺出之條件隊列前面三篇文章如果之前沒有基礎的話看起來會比較吃力,這篇文章說明一下的基礎知識,方便快速了解。當前節點由于超時或者中斷被取消,節點進入這個狀態以后將保持不變。 之前分析了AQS中的獨占鎖,共享鎖,條件隊列三大模塊,現在從結構上來看看AQS各個組件的情況。 原文地址:http://www.jianshu.com/p/49b8...
摘要:獲取鎖的過程當線程調用申請獲取鎖資源,如果成功,則進入臨界區。如果隊列中有其他等待鎖資源的線程需要喚醒,則喚醒隊列中的第一個等待節點先入先出。釋放鎖時,如果隊列中有等待的線程就進行喚醒。 每一個Java工程師應該都或多或少了解過AQS,我自己也是前前后后,反反復復研究了很久,看了忘,忘了再看,每次都有不一樣的體會。這次趁著寫博客,打算重新拿出來系統的研究下它的源碼,總結成文章,便于以后...
摘要:與之相關的方法有三個原子性地修改都是類型,可見我們可以進行,來定義的獲取與釋放從而實現我們自定義的同步器。 前言 源碼分析我認為主要有兩個作用:滿足好奇心,我想每一個有追求的人都不會滿足于僅僅做一個API Caller實現功能就好,我們也想知道它到底是怎么實現的;借鑒與升華,當我們明白了一個類的設計原理,在一定的情境下我們可以借鑒其設計哲學,甚至針對我們自己特殊的業務場景對其進行改良與...
摘要:與之相關的方法有三個原子性地修改都是類型,可見我們可以進行,來定義的獲取與釋放從而實現我們自定義的同步器。 前言 源碼分析我認為主要有兩個作用:滿足好奇心,我想每一個有追求的人都不會滿足于僅僅做一個API Caller實現功能就好,我們也想知道它到底是怎么實現的;借鑒與升華,當我們明白了一個類的設計原理,在一定的情境下我們可以借鑒其設計哲學,甚至針對我們自己特殊的業務場景對其進行改良與...
摘要:與之相關的方法有三個原子性地修改都是類型,可見我們可以進行,來定義的獲取與釋放從而實現我們自定義的同步器。 前言 源碼分析我認為主要有兩個作用:滿足好奇心,我想每一個有追求的人都不會滿足于僅僅做一個API Caller實現功能就好,我們也想知道它到底是怎么實現的;借鑒與升華,當我們明白了一個類的設計原理,在一定的情境下我們可以借鑒其設計哲學,甚至針對我們自己特殊的業務場景對其進行改良與...
閱讀 763·2019-08-29 12:49
閱讀 3550·2019-08-29 11:32
閱讀 3434·2019-08-26 10:43
閱讀 2402·2019-08-23 16:53
閱讀 2048·2019-08-23 15:56
閱讀 1695·2019-08-23 12:03
閱讀 2767·2019-08-23 11:25
閱讀 2084·2019-08-22 15:11