【JDK1.8】JUC——AbstractQueuedSynchronizer
目錄
一、前言
二、結構概覽
三、原始碼閱讀
3.1 tryAcquire(int arg)
3.2 addWaiter(Node mode)
3.3 acquireQueued(final Node node, int arg)
3.4 cancelAcquire(Node node)
3.5 release(int arg)
四、總結
一、前言
在上一篇中,我們對LockSupport進行了閱讀,因為它是實現我們今天要分析的AbstractQueuedSynchronizer(簡稱AQS)的基礎,重新用一下最開始的圖:
可以看到,在ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock中都用到了繼承自AQS的Sync內部類,正如AQS的java doc中一開始描述:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
為實現依賴於先進先出(FIFO)等待佇列的阻塞鎖和相關同步器(訊號量,事件等)提供框架。
AQS根據模式的不同:獨佔(EXCLUSIVE)和共享(SHARED)模式。
- 獨佔:只有一個執行緒能執行。如ReentrantLock。
- 共享:多個執行緒可同時執行。如Semaphore,可以設定指定數量的執行緒共享資源。
對應的類根據不同的模式,來實現對應的方法。
二、結構概覽
試想一下鎖的應用場景,當執行緒試圖請求資源的時候,先呼叫lock,如果獲得鎖,則得以繼續執行,而沒有獲得,則排隊阻塞,直到鎖被其他執行緒釋放,聽起來就像是一個列隊的結構。而實際上AQS底層就是一個先進先出的等待佇列:
佇列採用了連結串列的結構,node作為基本結構,主要有以下幾個成員變數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | static final class Node { //用來表明當前節點的等待狀態,主要有下面幾個: // CANCELLED: 1, 表示當前的執行緒被取消 // SIGNAL: -1, 表示後繼節點需要執行,也就是unpark // CONDITION: -2, 表示執行緒在等待condition // PROPAGATE: -3, 表示後續的acquireShared能夠得以執行,在共享模式中用到,後面會說 // 0, 初始狀態,在佇列中等待 volatile int waitStatus; // 指向前一個node volatile Node prev; // 指向後一個node volatile Node next; // 指向等待的那個執行緒 volatile Thread thread; // 在condition中用到 Node nextWaiter; } |
在AQS中,用head,tail來記錄了佇列的頭和尾,方便快速操作佇列:
1 2 3 4 5 6 | public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { private transient volatile Node head; private transient volatile Node tail; // 同步狀態 private volatile int state; } |
AQS的基本框架就是:state作為同步資源狀態,當執行緒請求鎖的時候,根據state數值判斷能否獲得鎖。不能,則加入佇列中等待。當持有鎖的執行緒釋放的時候,根據佇列裡的順序來決定誰先獲得鎖。
三、原始碼閱讀
獨佔模式典型的實現就是ReentrantLock,其具體流程如下:
獨佔模式下對應的lock-unlock就是acquire-release。整個過程如上圖所示。我們先來看一下acquire方法:
1 2 3 4 5 | public final void acquire( int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } |
- 呼叫tryAcquire(),該方法會在獨佔模式下嘗試請求獲取物件狀態。具體的實現由實現類去決定。
- 如果tryAcquire()失敗,即返回false,則呼叫addWaiter函式,將當前執行緒標記為獨佔模式,加入佇列的尾部。
- 呼叫acquireQueued(),讓執行緒在佇列中等待獲取資源,一直獲取到資源後才返回。如果在等阿迪過程中被中斷過,則返回true,否則返回false
- 如果執行緒被中斷過,在獲取鎖之後,呼叫中斷
3.1 tryAcquire(int arg)
下面來具體看一下各個方法:
1 2 3 | protected boolean tryAcquire( int arg) { throw new UnsupportedOperationException(); } |
前面說過了,AQS提供的是框架,其具體的實現由實現類來完成,tryAcquire就是其中之一,需要子類自己實現的方法,那既然要自己實現,為什麼不加abstract關鍵字,因為前面提到過,只有獨佔模式的實現類才需要實現這個方法,像Semaphore,CountDownLatch等共享模式的類不需要用到這個方法。如果加了關鍵字,那麼這些類還要實現,顯得很雞肋。
3.2 addWaiter(Node mode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private Node addWaiter(Node mode) { // 將當前執行緒封裝進node Node node = new Node(Thread.currentThread(), mode); Node pred = tail; // 插入佇列尾部,並維持節點前後關係 if (pred != null ) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 上一步如果失敗,在enq中繼續處理 enq(node); return node; } |
邏輯相對簡單,其中compareAndSetTail採用Unsafe類來實現。那麼下面的enq()方法是具體做了什麼呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private Node enq( final Node node) { for (;;) { Node t = tail; // 佇列初始化 if (t == null ) { if (compareAndSetHead( new Node())) tail = head; // 重複執行插入直到return } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } |
enq()方法為了防止在addWaiter中,節點插入佇列失敗沒有return,或者佇列沒有初始化,在for迴圈中反覆執行,確保插入成功,返回節點。
3.3 acquireQueued(final Node node, int arg)
到目前為止,走到acquireQueued()呼叫了前兩個方法,意味著獲取資源失敗,將節點加入了等待佇列,那麼下面要做的就是阻塞當前的執行緒,等待資源被是否後,再次喚醒執行緒來取得資源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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; } // 不符合上面的條件,那麼只能被park,等待被喚醒 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true ; } } finally { if (failed) cancelAcquire(node); } } |
acquireQueued當中,用for迴圈來讓執行緒等待,直至獲得資源return。而return的條件就是當前的節點是第二個節點,且頭結點已經釋放了資源。
再來看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
先來說一下parkAndCheckInterrupt:
1 2 3 4 | private final boolean parkAndCheckInterrupt() { LockSupport.park( this ); return Thread.interrupted(); } |
呼叫LockSupport.park,阻塞當前執行緒,當執行緒被重新喚醒後,返回是否被中斷過。
再來重點看一下shouldParkAfterFailedAcquire:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 獲取前一個節點的狀態 int ws = pred.waitStatus; // 如果前一個節點的狀態是signal,前面提到表明會unpark下一個節點,則true if (ws == Node.SIGNAL) return true ; // 如果ws > 0 即CANCELLED,則向前找,直到找到正常狀態的節點。 if (ws > 0 ) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0 ); // 維護正常狀態 pred.next = node; // 將前一個節點設定為SIGNAL } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false ; } |
shouldParkAfterFailedAcquire的主要作用就是將node放置在SIGNAL狀態的前節點下,確保能被喚醒,在呼叫該方法後,CANCELLED狀態的節點因為沒有引用執行它將被GC。
那麼問題來了,什麼時候節點會被設定為CANCELLED狀態?
答案就在try-finally的cancelAcquire(node)當中。當在acquireQueued取鎖的過程中,丟擲了異常,則會呼叫cancelAcquire。將當前節點的狀態設定為CANCELLED。
3.4 cancelAcquire(Node node)
我們先來看一下它的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | private void cancelAcquire(Node node) { // node為空,啥都不幹 if (node == null ) return ; node.thread = null ; // while查詢,直到找到非CANCELLED的節點 Node pred = node.prev; while (pred.waitStatus > 0 ) node.prev = pred = pred.prev; // 獲取非CANCELLED的節點的下一個節點,predNext肯定是CANCELLED Node predNext = pred.next; // 設定當前節點為CANCELLED狀態 node.waitStatus = Node.CANCELLED; // 如果節點在佇列尾部,直接移除自己就可以了 if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null ); } else { int ws; // 重新維護剩下的連結串列關係 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null ) { Node next = node.next; if (next != null && next.waitStatus <= 0 ) compareAndSetNext(pred, predNext, next); } else { // 喚醒node的下一個節點 unparkSuccessor(node); } // help GC node.next = node; } } |
總結來說,cancelAcquire就是用來維護連結串列正常狀態的關係,直接看程式碼認識起來可能還比較模糊,放圖:
幾個注意點:
- 如果node為第二個節點的時候,pred == head,喚醒下一個節點next_node,next_node執行緒會繼續在acquireQueued的for迴圈中執行,呼叫shouldParkAfterFailedAcquire會重新維護狀態,排除node節點
- 呼叫if裡的邏輯後,可以看到next的prev還指向node,會導致node無法被gc,這一點不用擔心,當next呼叫setHead被設定為head的時候,next的prev會被設定為null,這樣node就會被gc
1 2 3 4 5 | private void setHead(Node node) { head = node; node.thread = null ; node.prev = null ; } |
以上部分就是acquire的所有部分,建議忘記的園友們可以回到上面重新看一下流程圖,再接著穩固一遍。
3.5 release(int arg)
下面開始release的原始碼解析,相對於acquire來說要簡單一些:
1 2 3 4 5 6 7 8 9 | 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由實現類自己實現,如果為true,則unpark佇列頭部的下一個節點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private void unparkSuccessor(Node node) { // 清楚小於0的狀態 int ws = node.waitStatus; if (ws < 0 ) compareAndSetWaitStatus(node, ws, 0 ); // 如果下一個節點是CANCELLED,則從尾部向頭部找距離node最近的非CANCELLED節點 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; } // unpark找到的節點 if (s != null ) LockSupport.unpark(s.thread); } |
至此acuqire-release的部分就此結束了,至於共享模式的程式碼大同小異,在後面分析訊號量的時候會再提及~
四、總結
AQS應該是整個JUC中各個類涉及最多的了,其重要性可想而知,在瞭解其實現原理後,有助於我們分析其他的程式碼。最後謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!
http://www.wgt9662.top/
http://www.bux1348.top/
http://www.ukr4854.top/
http://www.cfs8763.top/
http://www.psd1092.top/
http://www.xck1603.top/
http://www.fgm4024.top/
http://www.zoj1707.top/
http://www.oiv1998.top/
http://www.ftw8814.top/
http://www.jfs6888.top/
http://www.kdx4817.top/
http://www.sbx6519.top/
http://www.rrq5611.top/
http://www.pxk9336.top/
http://www.vik6796.top/
http://www.kod8371.top/
http://www.nuq3623.top/
http://www.vfv3740.top/
http://www.tbt7039.top/
http://www.wky3695.top/
http://www.kcs3342.top/
http://www.gum4900.top/
http://www.mrw5927.top/
http://www.wnu1861.top/
http://www.vlc4617.top/
http://www.idv6045.top/
http://www.jmk5203.top/
http://www.mug5965.top/
http://www.gtt6107.top/
http://www.cnp6436.top/
http://www.sdx1013.top/
http://www.jwd3113.top/
http://www.qeu2095.top/
http://www.tux4376.top/
http://www.tay3928.top/
http://www.tgq6935.top/
http://www.win4778.top/
http://www.ngh4321.top/
http://www.cqq1459.top/
http://www.fxm1291.top/
http://www.wyz5825.top/
http://www.kbx3827.top/
http://www.tqt8862.top/
http://www.yma5505.top/
http://www.rye6349.top/
http://www.xqs4260.top/
http://www.fjv3790.top/
http://www.wqv1289.top/
http://www.mxz6626.top/
http://www.npl2536.top/
http://www.vme0237.top/
http://www.edr0603.top/
http://www.kft8502.top/
http://www.kwb2561.top/
http://www.dqv1869.top/
http://www.iai9521.top/
http://www.jla2696.top/
http://www.vip1477.top/
相關文章
- 聊聊JUC包下的底層支撐類-AbstractQueuedSynchronizer(AQS)AQS
- AbstractQueuedSynchronizer(AQS)抽絲剝繭深入瞭解JUC框架原理AQS框架
- AbstractQueuedSynchronizer原理剖析
- AbstractQueuedSynchronizer筆記筆記
- AbstractQueuedSynchronizer ConditionObject解析Object
- AbstractQueuedSynchronizer原始碼解析原始碼
- AbstractQueuedSynchronizer(AQS)深入剖析AQS
- AbstractQueuedSynchronizer原始碼分析原始碼
- JUC
- 深入理解AbstractQueuedSynchronizer(AQS)AQS
- AbstractQueuedSynchronizer(AQS)原始碼解析AQS原始碼
- AbstractQueuedSynchronizer部分原始碼解析原始碼
- 同步器AbstractQueuedSynchronizer淺析
- 初識Lock與AbstractQueuedSynchronizer(AQS)AQS
- java併發神器 AQS(AbstractQueuedSynchronizer)JavaAQS
- JUC簡介
- JUC工具(LockSupport)
- JUC的概述
- JUC之Volatile
- AbstractQueuedSynchronizer 佇列同步器(AQS)佇列AQS
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(3)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(1)JDK原始碼
- JUC之Callable介面回顧和JUC輔助類
- JUC 程式設計程式設計
- JUC集合安全-Map
- JUC:05.CountDownLatchCountDownLatch
- JUC前置知識
- Java JUC PriorityBlockingQueue解析JavaBloC
- Java JUC CopyOnWriteArrayList 解析Java
- Java JUC ThreadPoolExecutor解析Javathread
- Java JUC ReentrantLock解析JavaReentrantLock
- Java JUC LockSupport概述Java
- Java JUC LinkedBlockingQueue解析JavaBloC
- Java JUC ReentrantReadWriteLock解析Java
- Java JUC ConcurrentLinkedQueue解析Java
- JUC筆記(4)筆記
- Java併發——AbstractQueuedSynchronizer(AQS)同步器JavaAQS