【轉載】硬核乾貨:5W字17張高清圖理解同步器框架AbstractQueuedSynchronizer
前提
併發編程大師Doug Lea在編寫JUC
(java.util.concurrent
)包的時候引入了java.util.concurrent.locks.AbstractQueuedSynchronizer
,其實是Abstract Queued Synchronizer
,也就是”基於隊列實現的抽象同步器”,一般我們稱之爲AQS
。其實Doug Lea
大神編寫AQS
是有嚴謹的理論基礎的,他的個人博客上有一篇論文《The java.util.concurrent Synchronizer Framewor》,可以在互聯網找到相應的譯文《JUC同步器框架》,如果想要深入研究AQS
必須要理解一下該論文的內容,然後結合論文內容詳細分析一下AQS
的源碼實現。本文在閱讀AQS
源碼的時候選用的JDK
版本是JDK11
。
出於寫作習慣,下文會把AbstractQueuedSynchronizer稱爲AQS、JUC同步器框或者同步器框架。
AQS的主要功能
AQS
是JUC
包中用於構建鎖或者其他同步組件(信號量、事件等)的基礎框架類。AQS
從它的實現上看主要提供了下面的功能:
- 同步狀態的原子性管理。
- 線程的阻塞和解除阻塞。
- 提供阻塞線程的存儲隊列。
基於這三大功能,衍生出下面的附加功能:
- 通過中斷實現的任務取消,此功能基於線程中斷實現。
- 可選的超時設置,也就是調用者可以選擇放棄等待任務執行完畢直接返回。
- 定義了
Condition接口
,用於支持管程形式的await/signal/signalAll
操作,代替了Object
類基於JNI
提供的wait/notify/notifyAll
。
AQS
還根據同步狀態的不同管理方式區分爲兩種不同的實現:獨佔狀態的同步器和共享狀態的同步器。
同步器框架基本原理
《The java.util.concurrent Synchronizer Framework》一文中其實有提及到同步器框架的僞代碼:
while (synchronization state does not allow acquire) { enqueue current thread if not already queued; possibly block current thread; } dequeue current thread if it was queued;
update synchronization state; if (state may permit a blocked thread to acquire){ unblock one or more queued threads; }
|
撇腳翻譯一下:
while(同步狀態申請獲取失敗){ if(當前線程未進入等待隊列){ 當前線程放入等待隊列; } 嘗試阻塞當前線程; } 當前線程移出等待隊列
更新同步狀態 if(同步狀態足夠允許一個阻塞的線程申請獲取){ 解除一個或者多個等待隊列中的線程的阻塞狀態; }
|
爲了實現上述操作,需要下面三個基本環節的相互協作:
- 同步狀態的原子性管理。
- 等待隊列的管理。
- 線程的阻塞與解除阻塞。
其實基本原理很簡單,但是爲了應對複雜的併發場景和併發場景下程序執行的正確性,同步器框架在上面的acquire
操作和release
操作中使用了大量的死循環和CAS
等操作,再加上Doug Lea
喜歡使用單行復雜的條件判斷代碼,如一個if
條件語句會包含大量操作,AQS
很多時候會讓人感覺實現邏輯過於複雜。
同步狀態管理
AQS
內部內部定義了一個32
位整型的state
變量用於保存同步狀態:
private volatile int state;
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) { return STATE.compareAndSet(this, expect, update); }
|
同步狀態state
在不同的實現中可以有不同的作用或者表示意義,這裏其實不能單純把它理解爲中文意義上的”狀態”,它可以代表資源數、鎖狀態等等,下文遇到具體的場景我們再分析它表示的意義。
CLH隊列與變體
CLH
鎖即Craig, Landin, and Hagersten (CLH) locks
,因爲它底層是基於隊列實現,一般也稱爲CLH
隊列鎖。CLH
鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋。從實現上看,CLH
鎖是一種自旋鎖,能確保無飢餓性,提供先來先服務的公平性。先看簡單的CLH
鎖的一個簡單實現:
public class CLHLock implements Lock {
AtomicReference<QueueNode> tail = new AtomicReference<>(new QueueNode());
ThreadLocal<QueueNode> pred; ThreadLocal<QueueNode> current;
public CLHLock() { current = ThreadLocal.withInitial(QueueNode::new); pred = ThreadLocal.withInitial(() -> null); }
@Override public void lock() { QueueNode node = current.get(); node.locked = true; QueueNode pred = tail.getAndSet(node); this.pred.set(pred); while (pred.locked) { } }
@Override public void unlock() { QueueNode node = current.get(); node.locked = false; current.set(this.pred.get()); }
static class QueueNode {
boolean locked; }
}
|
上面是一個簡單的CLH
隊列鎖的實現,內部類QueueNode
只使用了一個簡單的布爾值locked
屬性記錄了每個線程的狀態,如果該屬性爲true
,則相應的線程要麼已經獲取到鎖,要麼正在等待鎖,如果該屬性爲false
,則相應的線程已經釋放了鎖。新來的想要獲取鎖的線程必須對tail
屬性調用getAndSet()
方法,使得自身成爲隊列的尾部,同時得到一個指向前驅節點的引用pred
,最後線程所在節點在其前驅節點的locked
屬性上自旋,直到前驅節點釋放鎖。上面的實現是無法運行的,因爲一旦自旋就會進入死循環導致CPU
飆升,可以嘗試使用下文將要提到的LockSupport
進行改造。
CLH
隊列鎖本質是使用隊列(實際上是單向鏈表)存放等待獲取鎖的線程,等待的線程總是在其所在節點的前驅節點的狀態上自旋,直到前驅節點釋放資源。從實際來看,過度自旋帶來的CPU性能損耗比較大,並不是理想的線程等待隊列的實現。
基於原始的CLH
隊列鎖中提供的等待隊列的基本原理,**AQS
實現一種了CLH鎖隊列的變體(Variant)**。AQS
類的protected
修飾的構造函數裏面有一大段註釋用於說明AQS
實現的等待隊列的細節事項,這裏列舉幾點重要的:
AQS
實現的等待隊列沒有直接使用CLH
鎖隊列,但是參考了其設計思路,等待節點會保存前驅節點中線程的信息,內部也會維護一個控制線程阻塞的狀態值。
- 每個節點都設計爲一個持有單獨的等待線程並且”帶有具體的通知方式”的監視器,這裏所謂通知方式就是自定義喚醒阻塞線程的方式而已。
- 一個線程是等待隊列中的第一個等待節點的持有線程會嘗試獲取鎖,但是並不意味着它一定能夠獲取鎖成功(這裏的意思是存在公平和非公平的實現),獲取失敗就要重新等待。
- 等待隊列中的節點通過
prev
屬性連接前驅節點,通過next
屬性連接後繼節點,簡單來說,就是雙向鏈表的設計。
CLH
隊列本應該需要一個虛擬的頭節點,但是在AQS
中沒有直接提供虛擬的頭節點,而是延遲到第一次競爭出現的時候懶創建虛擬的頭節點(其實也會創建尾節點,初始化時頭尾節點是同一個節點)。
Condition
(條件)等待隊列中的阻塞線程使用的是相同的Node
結構,但是提供了另一個鏈表用來存放,Condition
等待隊列的實現比非Condition
等待隊列複雜。
線程阻塞與喚醒
線程的阻塞和喚醒在JDK1.5
之前,一般只能依賴於Object
類提供的wait()
、notify()
和notifyAll()
方法,它們都是JNI
方法,由JVM
提供實現,並且它們必須運行在獲取監視器鎖的代碼塊內(synchronized
代碼塊中),這個侷限性先不談性能上的問題,代碼的簡潔性和靈活性是比較低的。JDK1.5
引入了LockSupport
類,底層是基於Unsafe
類的park()
和unpark()
方法,提供了線程阻塞和喚醒的功能,它的機制有點像只有一個允許使用資源的信號量java.util.concurrent.Semaphore
,也就是一個線程只能通過park()
方法阻塞一次,只能調用unpark()
方法解除調用阻塞一次,線程就會喚醒(多次調用unpark()
方法也只會喚醒一次),可以想象是內部維護了一個0-1的計數器。
LockSupport
類如果使用得好,可以提供更靈活的編碼方式,這裏舉個簡單的使用例子:
public class LockSupportMain implements Runnable {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private Thread thread;
private void setThread(Thread thread) { this.thread = thread; }
public static void main(String[] args) throws Exception { LockSupportMain main = new LockSupportMain(); Thread thread = new Thread(main, "LockSupportMain"); main.setThread(thread); thread.start(); Thread.sleep(2000); main.unpark(); Thread.sleep(2000); }
@Override public void run() { System.out.println(String.format("%s-步入run方法,線程名稱:%s", FORMATTER.format(LocalDateTime.now()), Thread.currentThread().getName())); LockSupport.park(); System.out.println(String.format("%s-解除阻塞,線程繼續執行,線程名稱:%s", FORMATTER.format(LocalDateTime.now()), Thread.currentThread().getName())); }
private void unpark() { LockSupport.unpark(thread); } }
2019-02-25 00:39:57.780-步入run方法,線程名稱:LockSupportMain 2019-02-25 00:39:59.767-解除阻塞,線程繼續執行,線程名稱:LockSupportMain
|
LockSupport
類park()
方法也有帶超時的變體版本方法,遇到帶超時期限阻塞等待場景下不妨可以使用LockSupport#parkNanos()
。
獨佔線程的保存
AbstractOwnableSynchronizer
是AQS
的父類,一個同步器框架有可能在一個時刻被某一個線程獨佔,AbstractOwnableSynchronizer
就是爲所有的同步器實現和鎖相關實現提供了基礎的保存、獲取和設置獨佔線程的功能,這個類的源碼很簡單:
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { } private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }
protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; } }
|
它就提供了一個保存獨佔線程的變量對應的Setter
和Getter
方法,方法都是final
修飾的,子類只能使用不能覆蓋。
CLH隊列變體的實現
這裏先重點分析一下AQS
中等待隊列的節點AQS
的靜態內部類Node
的源碼:
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; 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; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() {}
Node(Node nextWaiter) { this.nextWaiter = nextWaiter; THREAD.set(this, Thread.currentThread()); }
Node(int waitStatus) { WAITSTATUS.set(this, waitStatus); THREAD.set(this, Thread.currentThread()); }
final boolean compareAndSetWaitStatus(int expect, int update) { return WAITSTATUS.compareAndSet(this, expect, update); } final boolean compareAndSetNext(Node expect, Node update) { return NEXT.compareAndSet(this, expect, update); } final void setPrevRelaxed(Node p) { PREV.set(this, p); }
private static final VarHandle NEXT; private static final VarHandle PREV; private static final VarHandle THREAD; private static final VarHandle WAITSTATUS; static { try { MethodHandles.Lookup l = MethodHandles.lookup(); NEXT = l.findVarHandle(Node.class, "next", Node.class); PREV = l.findVarHandle(Node.class, "prev", Node.class); THREAD = l.findVarHandle(Node.class, "thread", Thread.class); WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } } }
|
其中,變量句柄(VarHandle
)是JDK9
引入的新特性,其實底層依賴的還是Unsafe
的方法,筆者認爲可以簡單理解它爲Unsafe
的門面類,而定義的方法基本都是面向變量屬性的操作。這裏需要關注一下Node
裏面的幾個屬性:
waitStatus
:當前Node
實例的等待狀態,可選值有5個。
- 初始值整數0:當前節點如果不指定初始化狀態值,默認值就是0,側面說明節點正在等待隊列中處於等待狀態。
Node#CANCELLED
整數值1:表示當前節點實例因爲超時或者線程中斷而被取消,等待中的節點永遠不會處於此狀態,被取消的節點中的線程實例不會阻塞。
Node#SIGNAL
整數值-1:表示當前節點的後繼節點是(或即將是)阻塞的(通過LockSupport#park()
),當它釋放或取消時,當前節點必須LockSupport#unpark()
它的後繼節點。
Node#CONDITION
整數值-2:表示當前節點是條件隊列中的一個節點,當它轉換爲同步隊列中的節點的時候,狀態會被重新設置爲0。
Node#PROPAGATE
整數值-3:此狀態值通常只設置到調用了doReleaseShared()
方法的頭節點,確保releaseShared()
方法的調用可以傳播到其他的所有節點,簡單理解就是共享模式下節點釋放的傳遞標記。
prev
、next
:當前Node
實例的前驅節點引用和後繼節點引用。
thread
:當前Node
實例持有的線程實例引用。
nextWaiter
:這個值是一個比較容易令人生疑的值,雖然表面上它稱爲”下一個等待的節點”,但是實際上它有三種取值的情況。
- 值爲靜態實例
Node.EXCLUSIVE
(也就是null),代表當前的Node
實例是獨佔模式。
- 值爲靜態實例
Node.SHARED
,代表當前的Node
實例是共享模式。
- 值爲非
Node.EXCLUSIVE
和Node.SHARED
的其他節點實例,代表Condition等待隊列中當前節點的下一個等待節點。
Node
類的等待狀態waitStatus
理解起來是十分費勁的,下面分析AQS
其他源碼段的時候會標識此狀態變化的時機。
其實上面的Node
類可以直接拷貝出來當成一個新建的類,然後嘗試構建一個雙向鏈表自行調試,這樣子就能深刻它的數據結構。例如:
public class AqsNode {
static final AqsNode SHARED = new AqsNode(); static final AqsNode EXCLUSIVE = null;
static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;
volatile int waitStatus;
volatile AqsNode prev;
volatile AqsNode next;
volatile Thread thread;
AqsNode nextWaiter;
final boolean isShared() { return nextWaiter == SHARED; }
final AqsNode predecessor() { AqsNode p = prev; if (p == null) throw new NullPointerException(); else return p; }
AqsNode() { }
AqsNode(AqsNode nextWaiter) { this.nextWaiter = nextWaiter; THREAD.set(this, Thread.currentThread()); }
AqsNode(int waitStatus) { WAITSTATUS.set(this, waitStatus); THREAD.set(this, Thread.currentThread()); }
final boolean compareAndSetWaitStatus(int expect, int update) { return WAITSTATUS.compareAndSet(this, expect, update); }
final boolean compareAndSetNext(AqsNode expect, AqsNode update) { return NEXT.compareAndSet(this, expect, update); }
final void setPrevRelaxed(AqsNode p) { PREV.set(this, p); }
private static final VarHandle NEXT; private static final VarHandle PREV; private static final VarHandle THREAD; private static final VarHandle WAITSTATUS;
static { try { MethodHandles.Lookup l = MethodHandles.lookup(); NEXT = l.findVarHandle(AqsNode.class, "next", AqsNode.class); PREV = l.findVarHandle(AqsNode.class, "prev", AqsNode.class); THREAD = l.findVarHandle(AqsNode.class, "thread", Thread.class); WAITSTATUS = l.findVarHandle(AqsNode.class, "waitStatus", int.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } }
public static void main(String[] args) throws Exception { AqsNode head = new AqsNode(); AqsNode next = new AqsNode(AqsNode.EXCLUSIVE); head.next = next; next.prev = head; AqsNode tail = new AqsNode(AqsNode.EXCLUSIVE); next.next = tail; tail.prev = next; List<Thread> threads = new ArrayList<>(); for (AqsNode node = head; node != null; node = node.next) { threads.add(node.thread); } System.out.println(threads); } }
[null, Thread[main,5,main], Thread[main,5,main]]
|
實際上,AQS
中一共存在兩種等待隊列,其中一種是普通的同步等待隊列,這裏命名爲Sync Queue
,另一種是基於Sync Queue
實現的條件等待隊列,這裏命名爲Condition Queue
。
理解同步等待隊列
前面已經介紹完AQS
的同步等待隊列節點類,下面重點分析一下同步等待隊列的相關源碼,下文的Sync隊列、Sync Queue、同步隊列和同步等待隊列是同一個東西。首先,我們通過分析Node
節點得知Sync
隊列一定是雙向鏈表,AQS
中有兩個瞬時成員變量用來存放頭節點和尾節點:
private transient volatile Node head;
private transient volatile Node tail;
private static final VarHandle STATE; private static final VarHandle HEAD; private static final VarHandle TAIL;
static { try { MethodHandles.Lookup l = MethodHandles.lookup(); STATE = l.findVarHandle(AbstractQueuedSynchronizer.class, "state", int.class); HEAD = l.findVarHandle(AbstractQueuedSynchronizer.class, "head", Node.class); TAIL = l.findVarHandle(AbstractQueuedSynchronizer.class, "tail", Node.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } Class<?> ensureLoaded = LockSupport.class; }
private final void initializeSyncQueue() { Node h; if (HEAD.compareAndSet(this, null, (h = new Node()))) tail = h; }
private final boolean compareAndSetTail(Node expect, Node update) { return TAIL.compareAndSet(this, expect, update); }
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
|
當前線程加入同步等待隊列和同步等待隊列的初始化是同一個方法,前文提到過:同步等待隊列的初始化會延遲到第一次可能出現競爭的情況,這是爲了避免無謂的資源浪費,具體方法是addWaiter(Node mode)
:
private Node addWaiter(Node mode) { Node node = new Node(mode); for (;;) { Node oldTail = tail; if (oldTail != null) { node.setPrevRelaxed(oldTail); if (compareAndSetTail(oldTail, node)) { oldTail.next = node; return node; } } else { initializeSyncQueue(); } } }
|
在首次調用addWaiter()
方法,死循環至少執行兩輪再跳出,因爲同步隊列必須初始化完成後(第一輪循環),然後再把當前線程所在的新節點實例添加到等待隊列中再返(第二輪循環)當前的節點,這裏需要注意的是新加入同步等待隊列的節點一定是添加到隊列的尾部並且會更新AQS
中的tail屬性爲最新入隊的節點實例。
假設我們使用Node.EXCLUSIVE
模式把新增的等待線程加入隊列,例如有三個線程分別是thread-1
、thread-2
和thread-3
,線程入隊的時候都處於阻塞狀態,模擬一下依次調用上面的入隊方法的同步隊列的整個鏈表的狀態。
先是線程thread-1
加入等待隊列:
接着是線程thread-2
加入等待隊列:
最後是線程thread-3
加入等待隊列:
如果仔細研究會發現,如果所有的入隊線程都處於阻塞狀態的話,新入隊的線程總是添加到隊列的tail
節點,阻塞的線程總是”爭搶”着成爲head
節點,這一點和CLH
隊列鎖的阻塞線程總是基於前驅節點自旋以獲取鎖的思路是一致的。下面將會分析的獨佔模式與共享模式,線程加入等待隊列都是通過addWaiter()
方法。
理解條件等待隊列
前面已經相對詳細地介紹過同步等待隊列,在AQS
中還存在另外一種相對特殊和複雜的等待隊列-條件等待隊列。介紹條件等待隊列之前,要先介紹java.util.concurrent.locks.Condition
接口。
public interface Condition { void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll(); }
|
Condition
可以理解爲Object
中的wait()
、notify()
和notifyAll()
的替代品,因爲Object
中的相應方法是JNI
(Native
)方法,由JVM
實現,對使用者而言並不是十分友好(有可能伴隨JVM
版本變更而受到影響),而Condition
是基於數據結構和相應算法實現對應的功能,我們可以從源碼上分析其實現。
Condition
的實現類是AQS
的公有內部類ConditionObject
。ConditionObject
提供的入隊列方法如下:
public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; - 條件隊列的第一個節點 private transient Node firstWaiter; - 條件隊列的最後一個節點 private transient Node lastWaiter; public ConditionObject() { } private Node addConditionWaiter() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node t = lastWaiter; if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } } protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }
}
|
實際上,Condition
的所有await()
方法變體都調用addConditionWaiter()
添加阻塞線程到條件隊列中。我們按照分析同步等待隊列的情況,分析一下條件等待隊列。正常情況下,假設有2個線程thread-1
和thread-2
進入條件等待隊列,都處於阻塞狀態。
先是thread-1
進入條件隊列:
然後是thread-2
進入條件隊列:
條件等待隊列看起來也並不複雜,但是它並不是單獨存在和使用的,一般依賴於同步等待隊列,下面的一節分析Condition
的實現的時候再詳細分析。
獨佔模式與共享模式
前文提及到,同步器涉及到獨佔模型和共享模式。下面就針對這兩種模式詳細分析一下AQS
的具體實現源碼。
獨佔模式
AQS
同步器如果使用獨佔(EXCLUSIVE
)模式,那麼意味着同一個時刻,只有唯一的一個節點所在線程獲取(acuqire
)原子狀態status
成功,此時該線程可以從阻塞狀態解除繼續運行,而同步等待隊列中的其他節點持有的線程依然處於阻塞狀態。獨佔模式同步器的功能主要由下面的四個方法提供:
acquire(int arg)
:申請獲取arg
個原子狀態status
(申請成功可以簡單理解爲status = status - arg
)。
acquireInterruptibly(int arg)
:申請獲取arg
個原子狀態status
,響應線程中斷。
tryAcquireNanos(int arg, long nanosTimeout)
:申請獲取arg
個原子狀態status
,帶超時的版本。
release(int arg)
:釋放arg
個原子狀態status
(釋放成功可以簡單理解爲status = status + arg
)。
獨佔模式下,AQS
同步器實例初始化時候傳入的status
值,可以簡單理解爲”允許申請的資源數量的上限值”,下面的acquire
類型的方法暫時稱爲”獲取資源”,而release
方法暫時稱爲”釋放資源”。接着我們分析前面提到的四個方法的源碼,先看acquire(int arg)
:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
static void selfInterrupt() { Thread.currentThread().interrupt(); }
final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return interrupted; } if (shouldParkAfterFailedAcquire(p, node)) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { cancelAcquire(node); if (interrupted) selfInterrupt(); throw t; } }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { pred.compareAndSetWaitStatus(ws, Node.SIGNAL); } return false; }
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
|
上面的代碼雖然看起來能基本理解,但是最好用圖推敲一下”空間上的變化”:
接着分析一下release(int arg)
的實現:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) node.compareAndSetWaitStatus(ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node p = tail; p != node && p != null; p = p.prev) if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); }
|
接着用上面的圖:
上面圖中thread-2
晉升爲頭節點的第一個後繼節點,等待下一個release()
釋放資源喚醒之就能晉升爲頭節點,一旦晉升爲頭節點也就是意味着可以解除阻塞繼續運行。接着我們可以看acquire()
的響應中斷版本和帶超時的版本。先看acquireInterruptibly(int arg)
:
public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }
private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
|
doAcquireInterruptibly(int arg)
方法和acquire(int arg)
類似,最大的不同點在於阻塞線程解除阻塞後並不是正常繼續運行,而是直接拋出InterruptedException
異常。最後看tryAcquireNanos(int arg, long nanosTimeout)
的實現:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) { cancelAcquire(node); return false; } if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
|
tryAcquireNanos(int arg, long nanosTimeout)
其實和doAcquireInterruptibly(int arg)
類似,它們都響應線程中斷,不過tryAcquireNanos()
在獲取資源的每一輪循環嘗試都會計算剩餘可用的超時時間,只有同時滿足獲取失敗需要阻塞並且剩餘超時時間大於SPIN_FOR_TIMEOUT_THRESHOLD(1000納秒)
的情況下才會進行阻塞。
獨佔模式的同步器的一個顯著特點就是:頭節點的第一個有效(非取消)的後繼節點,總是嘗試獲取資源,一旦獲取資源成功就會解除阻塞並且晉升爲頭節點,原來所在節點會移除出同步等待隊列,原來的隊列長度就會減少1,然後頭結點的第一個有效的後繼節點繼續開始競爭資源。
使用獨佔模式同步器的主要類庫有:
- 可重入鎖
ReentrantLock
。
- 讀寫鎖
ReentrantReadWriteLock
中的寫鎖WriteLock
。
共享模式
共享(SHARED
)模式中的”共享”的含義是:同一個時刻,如果有一個節點所在線程獲取(acuqire
)原子狀態status
成功,那麼它會解除阻塞被喚醒,並且會把喚醒狀態傳播到所有有效的後繼節點(換言之就是喚醒整個同步等待隊列中的所有有效的節點)。共享模式同步器的功能主要由下面的四個方法提供:
acquireShared(int arg)
:申請獲取arg
個原子狀態status
(申請成功可以簡單理解爲status = status - arg
)。
acquireSharedInterruptibly(int arg)
:申請獲取arg
個原子狀態status
,響應線程中斷。
tryAcquireSharedNanos(int arg, long nanosTimeout)
:申請獲取arg
個原子狀態status
,帶超時的版本。
releaseShared(int arg)
:釋放arg
個原子狀態status
(釋放成功可以簡單理解爲status = status + arg
)。
先看acquireShared(int arg)
的源碼:
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean interrupted = false; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; return; } } if (shouldParkAfterFailedAcquire(p, node)) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { cancelAcquire(node); throw t; } finally { if (interrupted) selfInterrupt(); } }
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) continue; unparkSuccessor(h); } else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) continue; } if (h == head) break; } }
|
其實代碼的實現和獨佔模式有很多類似的地方,一個很大的不同點是:共享模式同步器當節點獲取資源成功晉升爲頭節點之後,它會把自身的等待狀態通過CAS
更新爲Node.PROPAGATE
,下一個加入等待隊列的新節點會把頭節點的等待狀態值更新回Node.SIGNAL
,標記後繼節點處於可以被喚醒的狀態,如果遇上資源釋放,那麼這個阻塞的節點就能被喚醒從而解除阻塞。我們還是畫圖理解一下,先假設tryAcquireShared(int arg)
總是返回小於0的值,入隊兩個阻塞的線程thread-1
和thread-2
,然後進行資源釋放確保tryAcquireShared(int arg)
總是返回大於0的值:
看起來和獨佔模式下的同步等待隊列差不多,實際上真正不同的地方在於有節點解除阻塞和晉升爲頭節點的過程。因此我們可以先看releaseShared(int arg)
的源碼:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
|
releaseShared(int arg)
就是在tryReleaseShared(int arg)
調用返回true
的情況下主動調用一次doReleaseShared()
從而基於頭節點傳播喚醒狀態和unpark
頭節點的後繼節點。接着之前的圖:
接着看acquireSharedInterruptibly(int arg)
的源碼實現:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
|
最後看tryAcquireSharedNanos(int arg, long nanosTimeout)
的源碼實現:
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout); }
private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.SHARED); try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; return true; } } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) { cancelAcquire(node); return false; } if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
|
共享模式的同步器的一個顯著特點就是:頭節點的第一個有效(非取消)的後繼節點,總是嘗試獲取資源,一旦獲取資源成功就會解除阻塞並且晉升爲頭節點,原來所在節點會移除出同步等待隊列,原來的隊列長度就會減少1,重新設置頭節點的過程會傳播喚醒的狀態,簡單來說就是喚醒一個有效的後繼節點,只要一個節點可以晉升爲頭節點,它的後繼節點就能被喚醒,以此類推。節點的喚醒順序遵循類似於FIFO的原則,通俗說就是先阻塞或者阻塞時間最長則先被喚醒。
使用共享模式同步器的主要類庫有:
- 信號量
Semaphore
。
- 倒數柵欄
CountDownLatch
。
Condition的實現
Condition
實例的建立是在Lock
接口的newCondition()
方法,它是鎖條件等待的實現,基於作用或者語義可以見Condition
接口的相關API註釋:
Condition是對象監視器鎖方法Object#wait()、Object#notify()和Object#notifyAll()的替代實現,對象監視器鎖實現鎖的時候作用的效果是每個鎖對象必須使用多個wait-set(JVM內置的等待隊列),通過Object提供的方法和監視器鎖結合使用就能達到Lock的實現效果。如果替換synchronized方法和語句並且結合使用Lock和Condition,就能替換並且達到對象監視器鎖的效果。
Condition
必須固有地綁定在一個Lock
的實現類上,也就是要通過Lock
的實例建立Condition
實例,而且Condition
的方法調用使用必須在Lock
的”鎖定代碼塊”中,這一點和synchronized
關鍵字以及Object
的相關JNI方法使用的情況十分相似。
前文介紹過Condition
接口提供的方法以及Condition
隊列,也就是條件等待隊列,通過畫圖簡單介紹了它的隊列節點組成。實際上,條件等待隊列需要結合同步等待隊列使用,這也剛好對應於前面提到的Condition
的方法調用使用必須在Lock
的鎖定代碼塊中。聽起來很懵逼,我們慢慢分析一下ConditionObject
的方法源碼就能知道具體的原因。
先看ConditionObject#await()
方法:
private static final int REINTERRUPT = 1;
private static final int THROW_IE = -1;
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
final int fullyRelease(Node node) { try { int savedState = getState(); if (release(savedState)) return savedState; throw new IllegalMonitorStateException(); } catch (Throwable t) { node.waitStatus = Node.CANCELLED; throw t; } }
final boolean isOnSyncQueue(Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null) return false; if (node.next != null) return true; return findNodeFromTail(node); }
private boolean findNodeFromTail(Node node) { for (Node p = tail;;) { if (p == node) return true; if (p == null) return false; p = p.prev; } }
private int checkInterruptWhileWaiting(Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; }
final boolean transferAfterCancelledWait(Node node) { if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) { enq(node); return true; } while (!isOnSyncQueue(node)) Thread.yield(); return false; }
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt(); }
|
其實上面的await()
邏輯並不複雜,前提是理解了對象監視器鎖那套等待和喚醒的機制(由JVM
實現,C
語言學得好的可以去看下源碼),這裏只是通過算法和數據結構重新進行了一次實現。await()
主要使用了兩個隊列:同步等待隊列和條件等待隊列。我們先假設有兩個線程thread-1
和thread-2
調用了下面的代碼中的process()
方法:
ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition();
public void process(){ try{ lock.lock(); condition.await(); }finally{ lock.unlock(); } }
|
ReentrantLock
使用的是AQS
獨佔模式的實現,因此在調用lock()
方法的時候,同步等待隊列的一個瞬時快照(假設線程thread-1
先加入同步等待隊列)可能如下:
接着,線程thread-1
所在節點是頭節點的後繼節點,獲取鎖成功,它解除阻塞後可以調用await()
方法,這個時候會釋放同步等待隊列中的所有等待節點,也就是線程thread-2
所在的節點也被釋放,因此線程thread-2
也會調用await()
方法:
只要有線程能夠到達await()
方法,那麼原來的同步器中的同步等待隊列就會釋放所有阻塞節點,表現爲釋放鎖,然後這些釋放掉的節點會加入到條件等待隊列中,條件等待隊列中的節點也是阻塞的,這個時候只有通過signal()
或者signalAll()
進行隊列元素轉移纔有機會喚醒阻塞的線程。因此接着看signal()
和signalAll()
的源碼實現:
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }
final boolean transferForSignal(Node node) { if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) return false; Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignalAll(first); }
private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); }
|
其實signal()
或者signalAll()
會對取消的節點或者短暫中間狀態的節點進行解除阻塞,但是正常情況下,它們的操作結果是把阻塞等待時間最長的一個或者所有節點重新加入到AQS
的同步等待隊列中。例如,上面的例子調用signal()
方法後如下:
這樣子,相當於線程thread-1
重新加入到AQS
同步等待隊列中(從條件等待隊列中移動到同步等待隊列中),並且開始競爭頭節點,一旦競爭成功,就能夠解除阻塞。這個時候從邏輯上看,signal()
方法最終解除了對線程thread-1
的阻塞。await()
的其他變體方法的原理是類似的,這裏因爲篇幅原因不再展開。這裏小結一下Condition
的顯著特點:
- 1、同時依賴兩個同步等待隊列,一個是
AQS
提供,另一個是ConditionObject
提供的。
- 2、
await()
方法會釋放AQS
同步等待隊列中的阻塞節點,這些節點會加入到條件等待隊列中進行阻塞。
- 3、
signal()
或者signalAll()
會把條件等待隊列中的節點重新加入AQS
同步等待隊列中,並不解除正常節點的阻塞狀態。
- 4、接第3步,這些進入到
AQS
同步等待隊列的節點會重新競爭成爲頭節點,接下來的步驟其實也就是前面分析過的獨佔模式下的AQS
的運作原理。
取消獲取資源(cancelAcquire)
新節點加入等待隊列失敗導致任何類型的異常或者帶超時版本的API調用的時候剩餘超時時間小於等於零的時候,就會調用cancelAcquire()
方法,用於取消該節點對應節點獲取資源的操作。
private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; node.waitStatus = Node.CANCELLED; if (node == tail && compareAndSetTail(node, pred)) { pred.compareAndSetNext(predNext, null); } else { int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) pred.compareAndSetNext(predNext, next); } else { unparkSuccessor(node); } node.next = node; } }
|
cancelAcquire()
方法有多處調用,主要包括下面的情況:
- 1、節點線程在阻塞過程中主動中斷的情況下會調用。
- 2、
acquire
的處理過程發生任何異常的情況下都會調用,包括tryAcquire()
、tryAcquireShared()
等。
- 3、新節點加入等待隊列失敗導致任何類型的異常或者帶超時版本的API調用的時候剩餘超時時間小於等於零的時候。
cancelAcquire()
主要作用是把取消的節點移出同步等待隊列,必須時候需要進行後繼節點的喚醒。
實戰篇
AQS
是一個抽象的同步器基礎框架,其實我們也可以直接使用它實現一些高級的併發框架。下面基於AQS
實現一些非內建的功能,這兩個例子來自於AQS
的註釋中。
metux
大學C
語言課程中經常提及到的只有一個資源的metux
(互斥區),也就是說,同一個時刻,只能有一個線程獲取到資源,其他獲取資源的線程需要阻塞等待到前一個線程釋放資源。
public class Metux implements Lock, Serializable {
private static class Sync extends AbstractQueuedSynchronizer {
@Override protected boolean tryAcquire(int arg) { assert 1 == arg; if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }
@Override protected boolean tryRelease(int arg) { assert 1 == arg; if (!isHeldExclusively()) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; }
public Condition newCondition() { return new ConditionObject(); }
public boolean isLocked() { return getState() != 0; }
@Override public boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); }
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); } }
private final Sync sync = new Sync();
@Override public void lock() { sync.acquire(1); }
@Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
@Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); }
public boolean isLocked() { return sync.isLocked(); }
public boolean isHeldByCurrentThread() { return sync.isHeldExclusively(); }
@Override public void unlock() { sync.release(1); }
@Override public Condition newCondition() { return sync.newCondition(); }
public static void main(String[] args) throws Exception { final Metux metux = new Metux(); new Thread(() -> { metux.lock(); System.out.println(String.format("%s-thread-1獲取鎖成功休眠3秒...", LocalDateTime.now())); try { Thread.sleep(3000); } catch (InterruptedException e) { } metux.unlock(); System.out.println(String.format("%s-thread-1獲解鎖成功...", LocalDateTime.now())); return; }, "thread-1").start(); new Thread(() -> { metux.lock(); System.out.println(String.format("%s-thread-2獲取鎖成功...",LocalDateTime.now())); return; }, "thread-2").start(); Thread.sleep(Integer.MAX_VALUE); } }
|
某個時間的某次運行結果如下:
2019-04-07T11:49:27.858791200-thread-1獲取鎖成功休眠3秒... 2019-04-07T11:49:30.876567-thread-2獲取鎖成功... 2019-04-07T11:49:30.876567-thread-1獲解鎖成功...
|
二元柵欄
二元柵欄是CountDownLatch
的簡化版,只允許一個線程阻塞,由另一個線程負責喚醒。
public class BooleanLatch {
private static class Sync extends AbstractQueuedSynchronizer {
boolean isSignalled() { return getState() != 0; }
@Override protected int tryAcquireShared(int ignore) { return isSignalled() ? 1 : -1; }
@Override protected boolean tryReleaseShared(int ignore) { setState(1); return true; } }
private final Sync sync = new Sync();
public boolean isSignalled() { return sync.isSignalled(); }
public void signal() { sync.releaseShared(1); }
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
public static void main(String[] args) throws Exception { BooleanLatch latch = new BooleanLatch(); new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { } latch.signal(); }).start(); System.out.println(String.format("[%s]-主線程進入阻塞...", LocalDateTime.now())); latch.await(); System.out.println(String.format("[%s]-主線程進被喚醒...", LocalDateTime.now())); } }
|
某個時間的某次運行結果如下:
[2019-04-07T11:55:12.647816200]-主線程進入阻塞... [2019-04-07T11:55:15.632088]-主線程進被喚醒...
|
小結
在JUC
的重要併發類庫或者容器中,AQS
起到了基礎框架的作用,理解同步器的實現原理,有助於理解和分析其他併發相關類庫的實現。這篇文章前後耗費了接近1個月時間編寫,DEBUG
過程最好使用多線程斷點,否則很難模擬真實的情況。AQS
裏面的邏輯是相對複雜的,很敬佩併發大師Doug Lea
如此精巧的類庫設計,此所謂巨人的肩膀。
轉載地址
硬核乾貨:5W字17張高清圖理解同步器框架AbstractQueuedSynchronizerhttps://www.throwx.cn/2020/08/23/java-juc-aqs-source-code/)