【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)
概述
前文「JDK原始碼分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 節點的「獨佔模式」和「共享模式」,其實 AQS 也主要是圍繞對這兩種模式的操作進行的。
Node 節點是對執行緒 Thread 類的封裝,因此兩種模式可以理解如下:
獨佔模式(exclusive):執行緒對資源的訪問是排他的,即某個時間只能一個執行緒單獨訪問資源;
共享模式(shared):與獨佔模式不同,多個執行緒可以同時訪問資源。
本文先分析獨佔模式下的各種操作,後面再分析共享模式。
獨佔模式
方法概述
獨佔模式下的操作主要有以下幾個方法(可與前面分析的 Lock 介面的方法類比):
1. acquire(int arg)
以獨佔模式獲取資源,忽略中斷;可以類比 Lock 介面的 lock 方法;
2. acquireInterruptibly(int arg)
以獨佔模式獲取資源,響應中斷;可以類比 Lock 介面的 lockInterruptibly 方法;
3. tryAcquireNanos(int arg, long nanosTimeout)
以獨佔模式獲取資源,響應中斷,且有超時等待;可以類比 Lock 介面的 tryLock(long, TimeUnit) 方法;
4. release(int arg)
釋放資源,可以類比 Lock 介面的 unlock 方法。
方法分析
1. 獨佔模式獲取資源(忽略中斷)
這幾種獲取資源的方法很多地方是類似的。我們先從 acquire 方法開始分析,如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
該方法看似很短,其實是內部做了封裝。這幾行程式碼包含了如下四個操作步驟:
1. tryAcquire
2. addWaiter(Node.EXECUSIVE)
3. acquireQueued(final Node node, arg))
4. selfInterrupt
上面的四個步驟不一定全部執行,下面依次進行分析。
step 1: tryAcquire
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
該方法的作用是嘗試以獨佔模式獲取資源,若成功則返回 true。
可以看到該方法是一個 protected 方法,而且 AQS 中該方法直接丟擲了異常,其實是它把實現委託給了子類。這也是 ReentrantLock、CountdownLatch 等類(嚴格來說是其內部類 Sync)的實現功能不同的地方,這些類正是通過對該方法的不同實現來制定了自己的“遊戲規則”。
若 step 1 中的 tryAcquire 方法返回 true,則表示當前執行緒獲取資源成功,方法直接返回,該執行緒接下來就可以“為所欲為”了;否則表示獲取失敗,接下來會依次執行 step 2 和 step 3。
step 2: addWaiter(Node.EXECUSIVE)
private Node addWaiter(Node mode) { // 將當前執行緒封裝為一個 Node 節點,指定 mode // PS: 獨佔模式 Node.EXECUSIVE, 共享模式 Node.SHARED 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; // 通過 CAS 操作設定主佇列的尾節點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 尾節點 tail 為 null,表示主佇列未初始化 enq(node); return node; }
enq 方法:
private Node enq(final Node node) { for (;;) { Node t = tail; // 尾節點為空,表明當前佇列未初始化 if (t == null) { // Must initialize // 將佇列的頭尾節點都設定為一個新的節點 if (compareAndSetHead(new Node())) tail = head; } else { // 將 node 節點插入主佇列末尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
可以看到 addWaiter(Node.EXECUSIVE) 方法的作用是:把當前執行緒封裝成一個獨佔模式的 Node 節點,並插入到主佇列末尾(若主佇列未初始化,則將其初始化後再插入)。
step 3: acquireQueued(final Node node, arg))
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); } }
若當前節點的前驅節點為頭節點,則會再次嘗試獲取資源(tryAcuqire),若獲取成功,則將當前節點設定為頭節點並返回;否則,若前驅節點不是頭節點,或者獲取資源失敗,則執行如下兩個方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 前驅節點的等待狀態 int ws = pred.waitStatus; // 若前驅節點的等待狀態為 SIGNAL,返回 true,表示當前執行緒可以休眠 if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; // 若前驅節點的狀態大於 0,表示前驅節點處於取消(CANCELLED)狀態 // 則將前驅節點跳過(相當於踢出佇列) if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ 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. */ // 此時 waitStatus 只能為 0 或 PROPAGATE 狀態,將前驅節點的等著狀態設定為 SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
該方法的流程:
1. 若前驅節點的等待狀態為 SIGNAL,返回 true,表示當前執行緒可以休眠(park);
2. 若前驅節點是取消狀態 (ws > 0),則將其清理出佇列,以此類推;
3. 若前驅節點為 0 或 PROPAGATE,則將其設定為 SIGNAL 狀態。
正如其名,該方法(shouldParkAfterFailedAcquire)的作用就是判斷當前執行緒在獲取資源失敗後,是否可以休眠(park)。
parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() { // 將當前執行緒休眠 LockSupport.park(this); return Thread.interrupted(); }
該方法的作用:
1. 使當前執行緒休眠(park);
2. 返回該執行緒是否被中斷(其他執行緒對其發過中斷訊號)。
上面就是 acquireQueued(final Node node, arg)) 方法的執行過程,為了便於理解,可參考下面的流程圖:
若此期間被其他執行緒中斷過,則此時再去執行 selfInterrupt 方法去響應中斷請求:
static void selfInterrupt() { Thread.currentThread().interrupt(); }
以上就是 acquire 方法執行的整體流程。
2. 以獨佔模式獲取資源(響應中斷)
該操作其實與前面的過程類似,因此分析相對簡單些,程式碼如下:
public final void acquireInterruptibly(int arg) throws InterruptedException // 若執行緒被中斷過,則丟擲異常 if (Thread.interrupted()) throw new InterruptedException(); // 嘗試獲取資源 if (!tryAcquire(arg)) // 嘗試獲取資源失敗 doAcquireInterruptibly(arg); }
tryAcquire 與前面的操作一樣,若嘗試獲取資源成功則直接返回;否則,執行 doAcquireInterruptibly:
private void doAcquireInterruptibly(int arg) throws InterruptedException // 將當前執行緒封裝成 Node 節點插入主佇列末尾 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 丟擲中斷異常 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
通過與前面的 acquire 方法對比可以發現,二者程式碼幾乎一樣,區別在於 acquire 方法檢測到中斷(parkAndCheckInterrupt)時只是記錄了標誌位,並未響應;而此處直接丟擲了異常。這也是二者僅有的區別,此處不再詳細分析。
3. 以獨佔模式獲取資源(響應中斷,且有超時)
該操作與前者也是類似的,程式碼如下:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException // 若被中斷,則響應 if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }
doAcquireNanos:
static final long spinForTimeoutThreshold = 1000L; private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { // 若超時時間小於等於 0,直接獲取失敗 if (nanosTimeout <= 0L) return false; // 計算截止時間 final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } nanosTimeout = deadline - System.nanoTime(); // 已經超時了,獲取失敗 if (nanosTimeout <= 0L) return false; // 若大於自旋時間,則執行緒休眠;否則自旋 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); // 若被中斷,則響應 if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
這裡有個變數 spinForTimeoutThreshold,表示自旋時間,若大於該值則將執行緒休眠,否則繼續自旋。個人理解這裡增加該時間是為了提高效率,即,只有在等待時間較長的時候才讓執行緒休眠。
該方法與 acquireInterruptibly 也是類似的,在前者的基礎上增加了 timeout,不再詳細分析。
4. 釋放資源
前面分析了三種獲取資源的方式,自然也有釋放資源。下面分析釋放資源的 release 操作:
public final boolean release(int arg) { // 嘗試釋放資源,若成功則返回 true if (tryRelease(arg)) { Node h = head; // 若頭節點不為空,且等待狀態不為 0(此時為 SIGNAL) // 則喚醒其後繼節點 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
與 tryAcquire 方法類似,tryRelease 方法在 AQS 中也是丟擲異常,同樣交由子類實現:
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
unparkSuccessor 的主要作用是喚醒 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. */ 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; if (s == null || s.waitStatus > 0) { // 若後繼節點是取消狀態,則從尾節點向前遍歷,找到 node 節點後面一個未取消狀態的節點 s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // 喚醒node節點的後繼節點 if (s != null) LockSupport.unpark(s.thread); }
若 node 節點的後繼節點是取消狀態(ws > 0),則從主佇列中取其後面一個非取消狀態的執行緒喚醒。
前面三個獲取資源的方法中,finally 程式碼塊中都用到了 cancelAcquire 方法,都是獲取失敗時的操作,這裡也分析一下:
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. // 若該節點為尾節點(後面沒其他節點了),將 predNext 指向 null 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) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { // 前驅節點為頭節點,表明當前節點為第一個,取消時喚醒它的下一個節點 unparkSuccessor(node); } node.next = node; // help GC } }
該方法的主要操作:
1. 將 node 節點設定為取消(CANCELLED)狀態;
2. 找到它在佇列中非取消狀態的前驅節點 pred:
2.1 若 node 節點是尾節點,則前驅節點的後繼設為空,
2.2 若 pred 不是頭節點,且狀態為 SIGNAL,則後繼節點設為 node 的後繼節點;
2.3 若 pred 是頭節點,則喚醒 node 的後繼節點。
PS: 該過程可以跟雙鏈表刪除一個節點的過程進行對比分析。
小結
本文分析了以獨佔模式獲取資源的三種方式,以及釋放資源的操作。分別為:
1. acquire: 獨佔模式獲取資源,忽略中斷;
2. acquireInterruptibly: 獨佔模式獲取資源,響應中斷;
tryAcquireNanos: 獨佔模式獲取資源,響應中斷,有超時;
4. release: 釋放資源,喚醒主佇列中的下一個執行緒。
這幾個方法都可以類比 Lock 介面的相關方法定義。
相關閱讀:
JDK原始碼分析-AbstractQueuedSynchronizer(1)
JDK原始碼分析-Lock&Condition
Stay hungry, stay foolish.
PS: 本文首發於微信公眾號【WriteOnRead