java同步器AQS架構(gòu)AbstractQueuedSynchronizer原理解析下
引導(dǎo)語
AQS 的內(nèi)容太多,所以我們分成了兩個章節(jié),沒有看過 AQS 上半章節(jié)的同學(xué)可以回首看一下哈,上半章節(jié)里面說了很多鎖的基本概念,基本屬性,如何獲得鎖等等,本章我們主要聊下如何釋放鎖和同步隊列兩大部分。
1、釋放鎖
釋放鎖的觸發(fā)時機就是我們常用的 Lock.unLock () 方法,目的就是讓線程釋放對資源的訪問權(quán)(流程見整體架構(gòu)圖紫色路線)。
釋放鎖也是分為兩類,一類是排它鎖的釋放,一類是共享鎖的釋放,我們分別來看下。
1.1、釋放排它鎖 release
排它鎖的釋放就比較簡單了,從隊頭開始,找它的下一個節(jié)點,如果下一個節(jié)點是空的,就會從尾開始,一直找到狀態(tài)不是取消的節(jié)點,然后釋放該節(jié)點,源碼如下:
// unlock 的基礎(chǔ)方法 public final boolean release(int arg) { // tryRelease 交給實現(xiàn)類去實現(xiàn),一般就是用當前同步器狀態(tài)減去 arg,如果返回 true 說明成功釋放鎖。 if (tryRelease(arg)) { Node h = head; // 頭節(jié)點不為空,并且非初始化狀態(tài) if (h != null && h.waitStatus != 0) // 從頭開始喚醒等待鎖的節(jié)點 unparkSuccessor(h); return true; } return false; } // 很有意思的方法,當線程釋放鎖成功后,從 node 開始喚醒同步隊列中的節(jié)點 // 通過喚醒機制,保證線程不會一直在同步隊列中阻塞等待 private void unparkSuccessor(Node node) { // node 節(jié)點是當前釋放鎖的節(jié)點,也是同步隊列的頭節(jié)點 int ws = node.waitStatus; // 如果節(jié)點已經(jīng)被取消了,把節(jié)點的狀態(tài)置為初始化 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 拿出 node 節(jié)點的后面一個節(jié)點 Node s = node.next; // s 為空,表示 node 的后一個節(jié)點為空 // s.waitStatus 大于0,代表 s 節(jié)點已經(jīng)被取消了 // 遇到以上這兩種情況,就從隊尾開始,向前遍歷,找到第一個 waitStatus 字段不是被取消的 if (s == null || s.waitStatus > 0) { s = null; // 這里從尾迭代,而不是從頭開始迭代是有原因的。 // 主要是因為節(jié)點被阻塞的時候,是在 acquireQueued 方法里面被阻塞的,喚醒時也一定會在 acquireQueued 方法里面被喚醒,喚醒之后的條件是,判斷當前節(jié)點的前置節(jié)點是否是頭節(jié)點,這里是判斷當前節(jié)點的前置節(jié)點,所以這里必須使用從尾到頭的迭代順序才行,目的就是為了過濾掉無效的前置節(jié)點,不然節(jié)點被喚醒時,發(fā)現(xiàn)其前置節(jié)點還是無效節(jié)點,就又會陷入阻塞。 for (Node t = tail; t != null && t != node; t = t.prev) // t.waitStatus <= 0 說明 t 沒有被取消,肯定還在等待被喚醒 if (t.waitStatus <= 0) s = t; } // 喚醒以上代碼找到的線程 if (s != null) LockSupport.unpark(s.thread); }
1.2、釋放共享鎖 releaseShared
釋放共享鎖的方法是 releaseShared,主要分成兩步:
tryReleaseShared 嘗試釋放當前共享鎖,失敗返回 false,成功走 2;
喚醒當前節(jié)點的后續(xù)阻塞節(jié)點,這個方法我們之前看過了,線程在獲得共享鎖的時候,就會去喚醒其后面的節(jié)點,方法名稱為:doReleaseShared。
我們一起來看下 releaseShared 的源碼:
// 共享模式下,釋放當前線程的共享鎖 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { // 這個方法就是線程在獲得鎖時,喚醒后續(xù)節(jié)點時調(diào)用的方法 doReleaseShared(); return true; } return false; }
2、條件隊列的重要方法
在看條件隊列的方法之前,我們先得弄明白為什么有了同步隊列,還需要條件隊列?
主要是因為并不是所有場景一個同步隊列就可以搞定的,在遇到鎖 + 隊列結(jié)合的場景時,就需要 Lock + Condition 配合才行,先使用 Lock 來決定哪些線程可以獲得鎖,哪些線程需要到同步隊列里面排隊阻塞;獲得鎖的多個線程在碰到隊列滿或者空的時候,可以使用 Condition 來管理這些線程,讓這些線程阻塞等待,然后在合適的時機后,被正常喚醒。
同步隊列 + 條件隊列聯(lián)手使用的場景,最多被使用到鎖 + 隊列的場景中。
所以說條件隊列也是不可或缺的一環(huán)。
接下來我們來看一下條件隊列一些比較重要的方法,以下方法都在 ConditionObject 內(nèi)部類中。
2.1、入隊列等待 await
// 線程入條件隊列 public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 加入到條件隊列的隊尾 Node node = addConditionWaiter(); // 標記位置 A // 加入條件隊列后,會釋放 lock 時申請的資源,喚醒同步隊列隊列頭的節(jié)點 // 自己馬上就要阻塞了,必須馬上釋放之前 lock 的資源,不然自己不被喚醒的話,別的線程永遠得不到該共享資源了 int savedState = fullyRelease(node); int interruptMode = 0; // 確認node不在同步隊列上,再阻塞,如果 node 在同步隊列上,是不能夠上鎖的 // 目前想到的只有兩種可能: // 1:node 剛被加入到條件隊列中,立馬就被其他線程 signal 轉(zhuǎn)移到同步隊列中去了 // 2:線程之前在條件隊列中沉睡,被喚醒后加入到同步隊列中去 while (!isOnSyncQueue(node)) { // this = AbstractQueuedSynchronizer$ConditionObject // 阻塞在條件隊列上 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 標記位置 B // 其他線程通過 signal 已經(jīng)把 node 從條件隊列中轉(zhuǎn)移到同步隊列中的數(shù)據(jù)結(jié)構(gòu)中去了 // 所以這里節(jié)點蘇醒了,直接嘗試 acquireQueued if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled // 如果狀態(tài)不是CONDITION,就會自動刪除 unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
await 方法有幾點需要特別注意:
上述代碼標記位置 A 處,節(jié)點在準備進入條件隊列之前,一定會先釋放當前持有的鎖,不然自己進去條件隊列了,其余的線程都無法獲得鎖了;上述代碼標記位置 B 處,此時節(jié)點是被 Condition.signal 或者 signalAll 方法喚醒的,此時節(jié)點已經(jīng)成功的被轉(zhuǎn)移到同步隊列中去了(整體架構(gòu)圖中藍色流程),所以可以直接執(zhí)行 acquireQueued 方法;Node 在條件隊列中的命名,源碼喜歡用 Waiter 來命名,所以我們在條件隊列中看到 Waiter,其實就是 Node。
await 方法中有兩個重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我們一一看下。
2.1.1、addConditionWaiter
addConditionWaiter 方法主要是把節(jié)點放到條件隊列中,方法源碼如下:
// 增加新的 waiter 到隊列中,返回新添加的 waiter // 如果尾節(jié)點狀態(tài)不是 CONDITION 狀態(tài),刪除條件隊列中所有狀態(tài)不是 CONDITION 的節(jié)點 // 如果隊列為空,新增節(jié)點作為隊列頭節(jié)點,否則追加到尾節(jié)點上 private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. // 如果尾部的 waiter 不是 CONDITION 狀態(tài)了,刪除 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } // 新建條件隊列 node Node node = new Node(Thread.currentThread(), Node.CONDITION); // 隊列是空的,直接放到隊列頭 if (t == null) firstWaiter = node; // 隊列不為空,直接到隊列尾部 else t.nextWaiter = node; lastWaiter = node; return node; }
整體過程比較簡單,就是追加到隊列的尾部,其中有個重要方法叫做 unlinkCancelledWaiters,這個方法會刪除掉條件隊列中狀態(tài)不是 CONDITION 的所有節(jié)點,我們來看下 unlinkCancelledWaiters 方法的源碼,如下:
2.1.2、unlinkCancelledWaiters
// 會檢查尾部的 waiter 是不是已經(jīng)不是CONDITION狀態(tài)了 // 如果不是,刪除這些 waiter private void unlinkCancelledWaiters() { Node t = firstWaiter; // trail 表示上一個狀態(tài),這個字段作用非常大,可以把狀態(tài)都是 CONDITION 的 node 串聯(lián)起來,即使 node 之間有其他節(jié)點都可以 Node trail = null; while (t != null) { Node next = t.nextWaiter; // 當前node的狀態(tài)不是CONDITION,刪除自己 if (t.waitStatus != Node.CONDITION) { //刪除當前node t.nextWaiter = null; // 如果 trail 是空的,咱們循環(huán)又是從頭開始的,說明從頭到當前節(jié)點的狀態(tài)都不是 CONDITION // 都已經(jīng)被刪除了,所以移動隊列頭節(jié)點到當前節(jié)點的下一個節(jié)點 if (trail == null) firstWaiter = next; // 如果找到上次狀態(tài)是CONDITION的節(jié)點的話,先把當前節(jié)點刪掉,然后把自己掛到上一個狀態(tài)是 CONDITION 的節(jié)點上 else trail.nextWaiter = next; // 遍歷結(jié)束,最后一次找到的CONDITION節(jié)點就是尾節(jié)點 if (next == null) lastWaiter = trail; } // 狀態(tài)是 CONDITION 的 Node else trail = t; // 繼續(xù)循環(huán),循環(huán)順序從頭到尾 t = next; } }
為了方便大家理解這個方法,畫了一個釋義圖,如下:
2.2、單個喚醒 signal
signal 方法是喚醒的意思,比如之前隊列滿了,有了一些線程因為 take 操作而被阻塞進條件隊列中,突然隊列中的元素被線程 A 消費了,線程 A 就會調(diào)用 signal 方法,喚醒之前阻塞的線程,會從條件隊列的頭節(jié)點開始喚醒(流程見整體架構(gòu)圖中藍色部分),源碼如下:
// 喚醒阻塞在條件隊列中的節(jié)點 public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 從頭節(jié)點開始喚醒 Node first = firstWaiter; if (first != null) // doSignal 方法會把條件隊列中的節(jié)點轉(zhuǎn)移到同步隊列中去 doSignal(first); } // 把條件隊列頭節(jié)點轉(zhuǎn)移到同步隊列去 private void doSignal(Node first) { do { // nextWaiter為空,說明到隊尾了 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; // 從隊列頭部開始喚醒,所以直接把頭節(jié)點.next 置為 null,這種操作其實就是把 node 從條件隊列中移除了 // 這里有個重要的點是,每次喚醒都是從隊列頭部開始喚醒,所以把 next 置為 null 沒有關(guān)系,如果喚醒是從任意節(jié)點開始喚醒的話,就會有問題,容易造成鏈表的割裂 first.nextWaiter = null; // transferForSignal 方法會把節(jié)點轉(zhuǎn)移到同步隊列中去 // 通過 while 保證 transferForSignal 能成功 // 等待隊列的 node 不用管他,在 await 的時候,會自動清除狀態(tài)不是 Condition 的節(jié)點(通過 unlinkCancelledWaiters 方法) // (first = firstWaiter) != null = true 的話,表示還可以繼續(xù)循環(huán), = false 說明隊列中的元素已經(jīng)循環(huán)完了 } while (!transferForSignal(first) && (first = firstWaiter) != null); }
我們來看下最關(guān)鍵的方法:transferForSignal。
// 返回 true 表示轉(zhuǎn)移成功, false 失敗 // 大概思路: // 1. node 追加到同步隊列的隊尾 // 2. 將 node 的前一個節(jié)點狀態(tài)置為 SIGNAL,成功直接返回,失敗直接喚醒 // 可以看出來 node 的狀態(tài)此時是 0 了 final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ // 將 node 的狀態(tài)從 CONDITION 修改成初始化,失敗返回 false if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // 當前隊列加入到同步隊列,返回的 p 是 node 在同步隊列中的前一個節(jié)點 // 看命名是 p,實際是 pre 單詞的縮寫 Node p = enq(node); int ws = p.waitStatus; // 狀態(tài)修改成 SIGNAL,如果成功直接返回 // 把當前節(jié)點的前一個節(jié)點修改成 SIGNAL 的原因,是因為 SIGNAL 本身就表示當前節(jié)點后面的節(jié)點都是需要被喚醒的 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // 如果 p 節(jié)點被取消,或者狀態(tài)不能修改成SIGNAL,直接喚醒 LockSupport.unpark(node.thread); return true; }
整個源碼下來,我們可以看到,喚醒條件隊列中的節(jié)點,實際上就是把條件隊列中的節(jié)點轉(zhuǎn)移到同步隊列中,并把其前置節(jié)點狀態(tài)置為 SIGNAL。
2.3、全部喚醒 signalAll
signalAll 的作用是喚醒條件隊列中的全部節(jié)點,源碼如下:
public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 拿到頭節(jié)點 Node first = firstWaiter; if (first != null) // 從頭節(jié)點開始喚醒條件隊列中所有的節(jié)點 doSignalAll(first); } // 把條件隊列所有節(jié)點依次轉(zhuǎn)移到同步隊列去 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { // 拿出條件隊列隊列頭節(jié)點的下一個節(jié)點 Node next = first.nextWaiter; // 把頭節(jié)點從條件隊列中刪除 first.nextWaiter = null; // 頭節(jié)點轉(zhuǎn)移到同步隊列中去 transferForSignal(first); // 開始循環(huán)頭節(jié)點的下一個節(jié)點 first = next; } while (first != null); }
從源碼中可以看出,其本質(zhì)就是 for 循環(huán)調(diào)用 transferForSignal 方法,將條件隊列中的節(jié)點循環(huán)轉(zhuǎn)移到同步隊列中去。
3、總結(jié)
AQS 源碼終于說完了,你都懂了么,可以在默默回憶一下 AQS 架構(gòu)圖,看看這張圖現(xiàn)在能不能看懂了。
以上就是java同步器AQS架構(gòu)AbstractQueuedSynchronizer原理解析下的詳細內(nèi)容,更多關(guān)于java同步器AbstractQueuedSynchronizer的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring MVC之WebApplicationContext_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了Spring MVC之WebApplicationContext的相關(guān)資料,需要的朋友可以參考下2017-08-08SpringBoot整合RabbitMQ實現(xiàn)消息確認機制
這篇文章主要介紹了SpringBoot整合RabbitMQ實現(xiàn)消息確認機制,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08Java Synchronize下的volatile關(guān)鍵字詳解
這篇文章主要介紹了Java Synchronize下的volatile關(guān)鍵字詳解,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03新手入門學(xué)習(xí)Spring Freemarker教程解析
這篇文章主要介紹了新手入門學(xué)習(xí)Freemarker教程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10