1.背景
1.AQS簡介
AQS全稱爲AbstractQueuedSynchronizer(抽象隊列同步器)。AQS是一個用來構建鎖和其他同步組件的基礎框架,
使用AQS可以簡單且高效地構造出應用廣泛的同步器,例如ReentrantLock、Semaphore、ReentrantReadWriteLock和FutureTask等等。
2.原理
AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。
如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,
即將暫時獲取不到鎖的線程加入到隊列中。
3.CLH隊列
CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。
AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。
2.重要成員變量介紹
2.1.state 表示鎖狀態
a.值爲0表示資源空閒可用,int型默認爲0;
b.值大於0表示資源忙,不可用,有線程持有這把鎖;
c.如果發生鎖重入則值+1,可以結合代碼分析;
d.注意不要把節點的等待狀態混淆在一起了,代碼示例:volatile int waitStatus;
state狀態變動的情況:
1.獲取鎖時,使用cas將0改爲1,代碼示例:compareAndSetState(0, 1)
2.鎖從入時,將累加鎖的狀態值,代碼示例:setState(nextc);
3.設置節點爲下一個獲取的節點,compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
2.2.head 和 tail 分別表示指向隊列的頭節點和尾節點的指針
AQS中的隊列是先進先出的雙向鏈表;
隊列中的頭節點並不是要獲取鎖的節點,只是佔位而已,真正要獲取鎖的節點是第二個節點,第二個節點獲取到鎖之後成爲頭節點;
某個線程沒有獲取到鎖則需要進入隊列中等候,持有鎖的線程一定不會在隊列中,可以結合後面的代碼分析
2.3.Node節點
隊列中的每個Node節點由主要由4個成員變量組成:
1.前一個節點指針
2.後一個節點指針
3.當前線程
4.當前線程的等待狀態(WaitStatus)
對於 waitStatus 枚舉值,記錄當前線程的等待狀態,
int型默認值爲0
CANCELLED (1)表示線程被取消了
SIGNAL (-1)表示線程需要被喚醒,處於等待狀態,即下一個獲取資源的線程
CONDITION (-2)表示線程在條件隊列裏面等待
PROPAGATE (-3)表示釋放共享資源時需要通知其他節點
注意:在後面的代碼分析中一定要注意waitStatus的狀態時如何修改的
簡要代碼如下:
/** * 雙向鏈表隊列節點 */ class Node { /** * 對於 waitStatus 枚舉值,記錄當前線程的等待狀態, * int型默認值爲0 * CANCELLED (1)表示線程被取消了 * SIGNAL (-1)表示線程需要被喚醒,處於等待狀態,即下一個獲取資源的線程 * CONDITION (-2)表示線程在條件隊列裏面等待 * PROPAGATE (-3)表示釋放共享資源時需要通知其他節點 */ static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; /** * 等待獲取資源的狀態 */ volatile int waitStatus; /** * 前一個節點 */ volatile Node prev; /** * 下一個節點 */ volatile Node next; /** * 節點對應的線程 */ volatile Thread thread; }
3.繼承體系
4.獲取鎖源碼分析
在分析源碼之前,我們再次回顧一下原理:
AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。
如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,
即將暫時獲取不到鎖的線程加入到隊列中。
用通俗的話說就是:
獲取鎖時如果資源空閒,就立即執行,執行完成後喚醒隊列中等待的線程;如果資源被佔用,就進入隊列等待;(知道了這個大體方向後,對代碼的理解非常有幫助)
源碼分析開始:
1.創建鎖對象調用lock方法
2.進入lock方法
3.公平鎖和非公平鎖選擇
大家在這裏可留意一下,公平鎖和非公平鎖是怎麼樣實現的的?
簡單補充一下:
公平鎖就是先到先獲得資源的意思,在獲取鎖時會判斷一下隊列中有沒有處於等待的線程(具體的代碼實現後面看,這裏簡單提一下);
非公平鎖是,只要能搶到鎖都可以;
由此可見,非公平鎖效率要高一點,實際生產中也是經常採用非公平鎖,
那麼問題又來了,我們在使用的時候怎麼設置使用公平鎖還是非公平鎖呢?(詳見後面代碼解讀)
4.調用acquire方法
傳入參數1,表示將AQS中的資源狀態state修改爲1,加鎖成功.
5.調用tryAcquire方法
6.進入公平鎖重寫的方法
注意:如果FairSync對象沒有重寫tryAcquire方法就會拋出異常
4.1.tryAcquire方法邏輯(嘗試獲取鎖)
/** * 嘗試獲取鎖 * * @param acquires * @return */ protected final boolean tryAcquire(int acquires) { // 獲取當前線程對象 final Thread current = Thread.currentThread(); // 獲取當前鎖的狀態 int c = getState(); if (c == 0) { // 狀態爲0表示資源可用 //解讀一: hasQueuedPredecessors()方法的作用,檢查對隊列中是否有需要等待執行的線程,這是公平鎖的體現,隊列中有待執行的線程,優先讓隊列中的線程執行; //解讀二: 如果hasQueuedPredecessors()這個方法返回false表示隊列中沒有等待執行的線程,那麼使用方法compareAndSetState(0, acquires) 進行cas機制修改資源狀態,修改成功表示獲取鎖成功,方法返回true。 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 設置當前線程對象爲 鎖擁有的線程對象 // 思考一下:爲什麼這個裏設置線程對象時不使用cas的方式修改呢?難道不怕線程併發引起的問題麼? // 答曰:不需要,因爲這裏線程已經獲取到鎖了,只有獲取到鎖的線程纔可以執行這裏的代碼,而當前獲取到鎖的線程只有一個線程,故無需使用cas的方式,即:不存在併發 setExclusiveOwnerThread(current); // 返回獲取鎖成功 return true; } // 如果 current == getExclusiveOwnerThread() 相等,說明當前線程與鎖對象擁有的線程是是同一個線程,則也可以認爲獲取鎖成功,即:重入鎖 } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 記錄鎖重入次數,大家可以對同一個線程在還沒有釋放鎖的時候多次獲取鎖,然後通過斷點的方式,觀察這裏的值變化。 setState(nextc); // 返回獲取鎖成功 return true; } // 返回獲取鎖失敗 return false; }
4.2.hasQueuedPredecessors方法邏輯(判定隊列中是否有待執行節點)
/** * 隊列中是否有等待處理的節點 * 根據AQS原理,在公平鎖的情況下,頭節點的下一個節點的線程不等於當前線程(s.thread != Thread.currentThread()),則表示隊列中有待執行的節點,返回true * * @return 返回true表示隊列中有等待處理的節點, 返回false表示沒有等待處理的節點 */ public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; /** * h != t,如果頭結點等於尾節點,說明隊列中無數據,返回false,隊列中沒有等待處理的節點, * (s = h.next) == null,頭節點的下一個節點爲空 返回true * s.thread != Thread.currentThread() 頭結點的下一個節點(即將執行的節點)所擁有的線程不是當前線程,返回true-->隊列中有即將執行的節點 */ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
4.3.addWaiter方法解讀(添加一個節點)
代碼:
/** * 將無法獲取鎖的線程加入隊列中 * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 判定是否有等待的線程 if (pred != null) { // 有等待的線程 // 將新節點的前一個節點的線程設置爲尾節點,通俗的理解就是 把新加入的節點掛在尾節點後 node.prev = pred; // 更新尾幾點,期望尾節點爲pred,更新爲node if (compareAndSetTail(pred, node)) { // 將尾節點的下一個節點的值設置爲 node,這裏是雙向鏈表 node.prev = pred; pred.next = node; 構成雙向鏈表 pred.next = node; // 設置成功返回節點 node return node; } } // 沒有等待的線程,將節點插入隊列,必要時進行初始化 enq(node); return node; }
圖解:
第一步:Node pred=tail; 將尾節點賦值給前置節點,意思結束pred指向t4;
第二步:node.pred=pred;設置新增節點t5的前置節點爲pred,意思就是說設置t5的前置節點爲t4;
注意:這裏新增一個節點的時候,是先設置前置節點,如果在併發的情況下,
cpu發生切換,雙向鏈表只能從後往前遍歷,這個在面試中經常問到,如下圖:
4.4.enq方法詳解(初始化節點)
代碼:
/** * 將節點插入隊列,必要時進行初始化 */ private Node enq(final Node node) { // 死循環,直到插入隊列成功 for (;;) { Node t = tail; // Must initialize , // 整理思路就是:當隊列還是空的時候,初始化一個頭結點和尾節點, // 注意實際上頭結點和尾節點中沒有線程,只是佔位 if (t == null) { // 尾節點爲空 // compareAndSetHead(new Node()) 設置頭結點 if (compareAndSetHead(new Node())) // 尾節點與頭結點是同一個節點 tail = head; } else { // 構成雙向鏈表 node.prev = t;t.next = node; node.prev = t; // 設置尾節點 if (compareAndSetTail(t, node)) { // 設置t節點的下一個節點爲node,此時的t節點其實就是空節點 t.next = node; return t; } } } }
圖解:
4.4.acquireQueued方法詳解
這個方法比較難理解
1.需要結合for自旋邏輯閱讀代碼
2.需要理解這三個方法的含義
shouldParkAfterFailedAcquire(p, node)檢查當前節點是否應該被,阻塞等待park
parkAndCheckInterrupt() 當前線程進入阻塞狀態
cancelAcquire(node); 取消節點
代碼:
/** * acquireQueued()用於隊列中的線程自旋地以獨佔且不可中斷的方式獲取同步狀態(acquire),直到拿到鎖之後再返回。 * 該方法的實現分成兩部分:如果當前節點已經成爲頭結點,嘗試獲取鎖(tryAcquire)成功,然後返回; * 否則檢查當前節點是否應該被park(等待), * 然後將該線程park並且檢查當前線程是否被可以被中斷。 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; // 是否需要被取消的標記,true表示需要取消獲取資源 try { boolean interrupted = false; // 是否需要中斷標記,這個中斷標記的作用 for (; ; ) { final Node p = node.predecessor(); // node 節點的前一個節點 // p == head 表示除節點node外,沒有等待的節點 // tryAcquire(arg) 當前線程 嘗試獲取鎖 if (p == head && tryAcquire(arg)) { // 將剛纔已經獲取了鎖的線程設置爲頭結點 // setHead(node),爲什麼這裏設置頭節點 不使用 cas的方式 setHead(node); p.next = null;// help GC // failed = false 說明,只有在出異常的情況下才會執行 cancelAcquire(node); failed = false; return interrupted; } // shouldParkAfterFailedAcquire(p, node)檢查當前節點是否應該被,阻塞等待park // parkAndCheckInterrupt() 當前線程進入阻塞狀態 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) // 只有在出異常的情況下才會執行到這裏 cancelAcquire(node); } }
4.5.shouldParkAfterFailedAcquire方法詳解
代碼解讀:
/** * 檢查並更新無法獲取的節點的狀態。 * 如果線程應該阻塞,則返回true。這是所有采集迴路中的主要信號控制。要求pred==node.prev。 * Node.SIGNAL=-1, waitStatus值,用於指示後續線程需要取消連接 * * 對於 waitStatus 枚舉值,記錄當前線程的等待狀態, * int型默認值爲0 * CANCELLED (1)表示線程被取消了 * SIGNAL (-1)表示線程需要被喚醒,處於等待狀態,即下一個獲取資源的線程 * CONDITION (-2)表示線程在條件隊列裏面等待 * PROPAGATE (-3)表示釋放共享資源時需要通知其他節點 */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. * 翻譯: 當前節點狀態是等待喚醒狀態(-1),後面的節點可以安全暫停(park) */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. * 翻譯:前置節點已經被取消(ws=1>0),跳過前置節點,並且重新設置前置節點 * 注意:這裏的思路是如果前置節點已經是取消狀態,就跳過當前的前置節點繼續向前找有效的前置節點 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. * 翻譯: 狀態是0或者-3,的情況下線,設置前置節點爲 -1 需要喚醒狀態 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 返回false會產生自旋,在下一次進入當前函數的時候就會返回true, // 這個方法一定要結合方法acquireQueued一起看 return false; }
圖解:
這個方法每次執行節點狀態是動態改變的,單用靜態圖不好畫,這裏只是畫出最核心的邏輯,
假設新增節點時node,前置節點時n3,並且n3是一個取消節點
4.6.parkAndCheckInterrupt方法詳解
/** * 該方法讓線程去休息,真正進入等待狀態。 * park()會讓當前線程進入waiting狀態。 * 在此狀態下,有兩種途徑可以喚醒該線程:1)被unpark();2)被interrupt()。 * 需要注意的是,Thread.interrupted()會清除當前線程的中斷標記位。 * 預備知識補充======================= * 2.預備知識 * 2.1.park、unpark、interrupt、isInterrupted、interrupted方法的理解 * 一:park、unpark * 1.park、unpark它不是Thread中的方法,而是LockSupport.park(),LockSupport是JUC中的對象; * 2.park可以讓線程暫停 (只有在isInterrupted狀態爲false的情況下才有效),unpark可以讓暫停的線程繼續執行; * <p> * 二:interrupt、isInterrupted、interrupted * 1.interrupt、isInterrupted、interrupted 它是Thread中的方法; * 2.interrupt 設置一箇中斷標記,interrupt()方法可以讓暫停的方法繼續執行,通俗直觀的理解是,設置打斷標記後,處於暫停狀態的線程就會被喚醒,如果是睡眠的就拋出中斷異常; * 3.isInterrupted 這個好理解就是查看當前線程的中斷標記; * 4.interrupted 這是一個靜態方法,只能這樣調用Thread.interrupted(),這個方法會返回當前interrupt狀態,如果interrupt=true會將其改變爲 false,反之則不成立; */ private final boolean parkAndCheckInterrupt() { // 讓線程處於等待狀態,前提是無中斷標記,這裏可以思考一下,暫停後什麼時候被喚醒呢? LockSupport.park(this); // Thread.interrupted() 返回當前的interrupted狀態,如果之前是true,即有中斷標記,修改爲false,即清除中斷標記 return Thread.interrupted(); }
4.7.cancelAcquire方法詳解
圖解:
這裏假設當前取消的節點時N4,且N3是已經取消的節點,其他節點都是正常節點
代碼解讀:
/** * 節點取消獲取資源 * @param node */ private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; // 找到有效的前置節點,從後往前找,這裏會跳過已經取消的節點 while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. // 找到有效前置節點的下一個節點 Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. // 設置節點狀態爲取消狀態 node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. // compareAndSetTail(node, pred) 設置尾節點爲有效前置節點 if (node == tail && compareAndSetTail(node, pred)) { // 如果取消節點時尾節點,有效前置節點的下一個節點設置爲空, compareAndSetNext(pred, predNext, null); } else { // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {//有效前置節點不是頭結點的情況下: 設置有效前置節點的下一個節點爲當前節點的下一個節點 compareAndSetNext(pred, predNext, next); Node next = node.next; if (next != null && next.waitStatus <= 0) // 設置有效前置節點的下一節點爲取消節點的下一個節點 compareAndSetNext(pred, predNext, next); } else {//有效前置節點[是]頭結點的情況下: 喚醒下一個應該執行的線程 unparkSuccessor(node); } node.next = node; // help GC } }
5.鎖釋放
5.1.release方法詳解
代碼解讀:
/** * 鎖釋放 並喚醒下一個節點 * * @param arg * @return */ public final boolean release(int arg) { // tryRelease(arg) 釋放鎖 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 喚醒頭結點的下一個節點 unparkSuccessor(h); return true; } return false; }
5.2.tryRelease方法詳解
代碼解讀:
/** * 已執行完成的線程,釋放資源 * * @param releases * @return */ protected final boolean tryRelease(int releases) { // 獲取當前資源狀態值 int c = getState() - releases; // 如果當前線程不是獲得資源的線程,拋出異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 資源釋放成功還是失敗標記 boolean free = false; if (c == 0) { // 等於0的情況下表示資源釋放成功,考慮到鎖重入的情況,就是說同一個線程多次獲取了同一把鎖 free = true; // 資源釋放成功 setExclusiveOwnerThread(null); } // 設置當前資源狀態 setState(c); return free; }
5.3.unparkSuccessor方法詳解
代碼解讀:
/** * Wakes up node's successor, if one exists. * 喚醒節點的後續節點 * * @param node the node */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. * 這句代碼的意思:如果當前節點狀態小於0(等待執行狀態),對節點進行初始化 */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; // 如果當前節點的下一個節點是null 或者 狀態大於0,說明當前節點的下一個節點不是有效節點 if (s == null || s.waitStatus > 0) { s = null; // 這個for循環大家得認真理解,含義是從尾節點開始向前找,找到最前面的狀態小於0的節點 // 提問這裏爲什麼要從後往前找,而不是從前往後找?從前往後找不是更簡單麼? for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 讓當前節點的下一個節點,繼續執行 // 這裏相當於是線程間通信,與之前方法中的parkAndCheckInterrupt()->LockSupport.park(this); 進行了喚醒操作,使流程完整 LockSupport.unpark(s.thread); }
6.總結
AQS的核心思想是當沒有獲取到資源的線程就在隊列中等待,已經獲取到資源的線程執行完成後就喚醒隊列中的下一個節點去獲取資源,以此循環;
根據這個設計思想,就涉及到:
如何讓一個Java線程暫停,
如何喚醒Java線程,
在併發的情況又涉及到,如何原子操作修改資源狀態
等待的線程是採用雙向鏈表作爲隊列的,因此涉及到數據結構雙向鏈表的應用
雙向鏈表就涉及到添加節點,刪除節點,遍歷節點等操作
因此AQS的實現是Java基礎知識的一個綜合應用,
如果我們能深入的理解,不僅僅是可以應付面試的提問,更重要的是我們可以應用其思想解決我們在實際開中遇到的業務問題;
舉一個我個人在實際開發中借鑑AQS思想解決的業務問題,
業務場景是這樣的,提供一個接口去查詢某個商品是否有貨,這個商品由多個供貨商供貨的,只要我查詢到有一個供貨商能夠供貨就表示該商品可以出售的.
最簡單的做法,當然循環查詢(http請求)每一個供貨商,直到找可以供貨的商家,就停止查詢,很明顯這樣的查詢效率是很低的...
我希望的是同時去查詢所有的供貨商,只要有一家供貨商返回了有貨,該接口就可以返回商品可售了
完美!