AbstractQueuedSynchronizer,簡稱 AQS,是一個用於構建鎖和同步器的框架。
上一篇文章 介紹了 AQS 的數據結構和獨佔模式的實現原理,本篇介紹 AQS 共享模式的實現原理。java
本文基於 jdk1.8.0_91
獨佔模式下,只要有一個線程佔有鎖,其餘線程試圖獲取該鎖將沒法取得成功。
共享模式下,多個線程獲取某個鎖可能(但不是必定)會得到成功。node
共享模式下獲取鎖/資源,無視中斷segmentfault
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared數據結構
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
嘗試獲取資源,具體資源獲取方式交由自定義同步器實現。框架
java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquireSharedoop
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
對於返回值:ui
進入同步隊列,自旋判斷是否能獲取鎖,不然進入阻塞。線程
/** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); // 在隊列中加入共享模式的節點 boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); // 若是上一個節點是頭結點,則嘗試獲取共享資源,返回剩餘的資源數量 if (r >= 0) { setHeadAndPropagate(node, r); // 設置當前節點爲新的頭節點(dummy node),並喚醒後繼共享節點 p.next = null; // help GC // 舊的頭節點出隊 if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && // 上一個節點不是頭節點,須要判斷是否進入阻塞:1. 不能進入阻塞,則重試獲取鎖。2. 進入阻塞 parkAndCheckInterrupt()) // 阻塞當前線程。當從阻塞中被喚醒時,檢測當前線程是否已中斷,並清除中斷狀態。接着繼續重試獲取鎖。 interrupted = true; // 標記當前線程已中斷 } } finally { if (failed) cancelAcquire(node); } }
共享模式的 doAcquireShared 方法,與獨佔模式的 acquireQueued 相似,節點加入同步隊列以後進行自旋,執行兩個判斷:code
不一樣的地方是:blog
也就是說:
將當前節點設置爲新的頭節點。
若是共享資源有盈餘,喚醒後續等待中的共享節點。
/** * Sets head of queue, and checks if successor may be waiting * in shared mode, if so propagating if either propagate > 0 or * PROPAGATE status was set. * * @param node the node * @param propagate the return value from a tryAcquireShared // 共享資源的剩餘數量 */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); // 判斷是否喚醒後繼節點 if (propagate > 0 || h == null || h.waitStatus < 0 || // 若無剩餘資源,則校驗舊的頭節點h的狀態(PROPAGATE或SIGNAL,均<0) (h = head) == null || h.waitStatus < 0) { // 若其餘線程修改了head,取新head做爲前繼節點來校驗 Node s = node.next; if (s == null || s.isShared()) // node.next != null 時,這裏限制了只會喚醒共享節點! doReleaseShared(); // 喚醒後繼節點 } }
若是知足下列條件能夠嘗試喚醒下一個節點:
可能會形成沒必要要的喚醒,可是通常發生在大量地爭奪 acquires/releases 之時,而這種狀況下,線程遲早都會被喚醒。
喚醒後繼節點(共享模式下,當前線程獲取鎖成功、釋放鎖以後,均可能會調用該方法)。
注意:頭節點是共享節點,可是這個方法不會區分後繼節點是不是共享節點。
java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared
/** * Release action for shared mode -- signals successor and ensures * propagation. (Note: For exclusive mode, release just amounts * to calling unparkSuccessor of head if it needs signal.) */ // 共享模式下的 release 操做:在足夠的資源下,喚醒後繼節點,傳播信息(資源盈餘,可共享) // 互斥模式下的 release 操做:只會喚醒隊列頭部須要喚醒的一個後繼節點(見 AbstractQueuedSynchronizer#unparkSuccessor) private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { // 頭節點不爲空,且具備後繼節點 int ws = h.waitStatus; if (ws == Node.SIGNAL) { // 若是頭節點狀態是 SIGNAL,嘗試改成 0 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases // CAS失敗,從新自旋 unparkSuccessor(h); // 喚醒head的後繼節點 } else if (ws == 0 && // 若是頭節點狀態是 0,嘗試改成 PROPAGATE。 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS // CAS失敗,從新自旋 } if (h == head) // loop if head changed // 校驗頭節點是否發生變化,若變化了則從新校驗最新頭節點的狀態 break; } }
代碼流程:
關於 PROPAGATE 狀態
爲何當前節點狀態由 0 改成 PROPAGATE 失敗,須要繼續自旋?
爲何當前節點狀態由 0 改成 PROPAGATE 成功,就再也不喚醒後繼節點了呢?
共享模式下釋放鎖/資源
java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { // 釋放共享鎖/資源 doReleaseShared(); // 釋放鎖/資源成功,喚醒隊列中的等待節點 return true; } return false; }
java.util.concurrent.locks.AbstractQueuedSynchronizer#tryReleaseShared
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
相關閱讀:
閱讀 JDK 源碼:AQS 中的獨佔模式
閱讀 JDK 源碼:AQS 中的共享模式
閱讀 JDK 源碼:AQS 對 Condition 的實現
做者:Sumkor