AbstractQueuedSynchronizer簡介
AbstractQueuedSynchronizer
抽象佇列同步器,簡稱為AQS
,可用於構建阻塞鎖或者其他相關同步器的基礎框,是Java併發包的基礎工具類。通過AQS
這個框架可以對同步狀態原子性管理、執行緒的阻塞和解除阻塞、佇列的管理進行統一管理。
AQS
是抽象類,並不能直接例項化,當需要使用AQS
的時候需要繼承AQS
抽象類並且重寫指定的方法,這些重寫方法包括執行緒獲取資源和釋放資源的方式(如ReentractLock通過分別重寫執行緒獲取和釋放資源的方式實現了公平鎖和非公平鎖),同時子類還需要負責共享變數state的維護,如當state為0時表示該鎖沒有被佔,大於0時候代表該鎖被一個或多個執行緒佔領(重入鎖),而佇列的維護(獲取資源失敗入隊、執行緒喚醒、執行緒的狀態等)不需要我們考慮,AQS
已經幫我們實現好了。AQS
的這種設計模式採用的正是模板方法模式。
總結起來子類的任務有:
- 通過
CAS
操作維護共享變數state
。 - 重寫資源的獲取方式。
- 重寫資源釋放的方式。
如果對CAS和Java記憶體模型還不清楚的,建議先了解這兩者之後再食用本文,效果更佳!CAS原理分析及ABA問題詳解 什麼是Java記憶體模型?
完成以上三個任務即可實現自己的鎖。
AQS
作為J.U.C
的工具類,面向的是需要實現鎖的實現者,而鎖面向的是鎖的使用者,這兩者的區別還是需要搞清楚的。
AQS資料結構
先看AQS
有哪些重要的成員變數。
// 頭結點,你直接把它當做 當前持有鎖的執行緒 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個連結串列
private transient volatile Node tail;
// 這個是最重要的,不過也是最簡單的,代表當前鎖的狀態,0代表沒有被佔用,大於0代表有執行緒持有當前鎖
// 之所以說大於0,而不是等於1,是因為鎖可以重入嘛,每次重入都加上1
private volatile int state;
// 代表當前持有獨佔鎖的執行緒,舉個最重要的使用例子,因為鎖可以重入
// reentrantLock.lock()可以巢狀呼叫多次,所以每次用這個來判斷當前執行緒是否已經擁有了鎖
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer
複製程式碼
然後再看看AQS
的內部結構,AQS
內部資料結構為一個雙向連結串列和一個單向連結串列,雙連結串列為同步佇列,佇列中的每個節點對應一個Node
內部類,AQS
通過控制連結串列的節點而達到阻塞、同步的目的,單連結串列為條件佇列,可以把同步佇列和條件佇列理解成儲存等待狀態的執行緒的佇列,但是條件佇列中的執行緒並不能直接去獲取資源,而要先從條件佇列轉到同步佇列中排隊獲取,同步佇列的喚醒結果是執行緒去嘗試獲取鎖,而條件佇列的喚醒結果是把執行緒從條件佇列移到同步佇列中,一個執行緒要麼是在同步佇列中,要麼是在條件佇列中,不可能同時存在這兩個佇列裡面。
Java阻塞狀態和等待狀態的執行緒從Linux核心來看,都是阻塞(等待)狀態,它們都會讓出CPU時間片。Java為了方便管理執行緒將“阻塞(等待)”狀態細分成了阻塞狀態和等待狀態,這兩個狀態的區別在於由誰去喚醒,是作業系統還是其他執行緒。Java執行緒請求某一個資源失敗的時候就會進入阻塞狀態,處於阻塞態的執行緒會不斷請求資源,一旦請求成功,就會進入就緒佇列,等待執行。而當執行緒呼叫
wait
、join
、pack
函式時候會進入等待狀態,需要其它執行緒顯性的喚醒否則會無限期的處於等待狀態。
Java執行緒6狀態圖:
內部類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;
//代表節點(執行緒)在 Condition queue中,等待某一條件
static final int CONDITION = -2;
//代表當前節點的後繼節點(執行緒)會傳傳播喚醒的操作,僅在共享模式下才有作用
static final int PROPAGATE = -3;
//代表當前節點的狀態,它的取值除了以上說的CANCELLED、SIGNAL、CONDITION、PROPAGATE,同時
//還可能為0,為0的時候代表當前節點在sync佇列中,阻塞著排隊獲取鎖。
volatile int waitStatus;
//當前節點的前驅節點
volatile Node prev;
//當前節點的後繼節點
volatile Node next;
//當前節點關聯的執行緒
volatile Thread thread;
//在condition佇列中的後繼節點
Node nextWaiter;
//判斷當前節點是否為共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回當前節點的前驅節點 沒有前驅節點則丟擲異常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
複製程式碼
每個執行緒都關聯一個節點,節點的狀態也代表著執行緒的狀態,AQS
通過對同步佇列的管理而達到對執行緒的管理。
AQS的功能
AQS
提供了2
大功能,基於雙連結串列的同步佇列和基於單連結串列的條件佇列,同步佇列維護的是阻塞狀態的執行緒對應的節點,這些執行緒都是阻塞著排隊獲取鎖的,條件佇列維護的是等待狀態的執行緒對應的節點。
同步佇列
AQS
提供了兩種方式去獲取資源,分別是共享模式和獨佔模式,但是一般鎖只會去繼承其中一種模式,不會在一個鎖裡同時存在共享模式和獨佔模式兩種模式。
資源指鎖、IO、Socket等
當一個執行緒以共享模式或獨佔模式去獲取資源的時候,如果獲取失敗則將該執行緒封裝成Node
節點(同時將該節點標識為共享模式或獨佔模式)加入到同步佇列的尾部,AQS
實時維護著這個同步佇列,這個佇列以FIFO(先進先出)來管理節點的排隊,即資源的轉移(獲取再釋放)的順序是從頭結點開始到尾節點。
共享模式和獨佔模式去獲取、釋放資源都分別對應著一套API
,以下分別分析這兩套API
獨佔模式即獲取資源的排他鎖,共享模式及獲取資源的共享鎖。
獨佔模式
獨佔模式即一個執行緒獲取到資源後,其他執行緒不能再對資源進行任何操作,只能阻塞獲得資源。
獲取資源
- 執行緒呼叫子類重寫的
tryAcquire
方法獲取資源,如果獲取成功,則流程結束,否則繼續往下執行。 - 呼叫
addWaiter
方法(詳細過程看下面的原始碼解析),將該線程封裝成Node節點,並新增到佇列隊尾。 - 呼叫
acquireQueued
方法讓節點以"死迴圈"方式進行獲取資源,為什麼死迴圈加了雙引號呢?因為迴圈並不是一直讓節點無間斷的去獲取資源,節點會經歷 獲取資源->失敗->執行緒進入等待狀態->喚醒->獲取資源......,執行緒在死迴圈的過程會不斷等待和喚醒,節點進入到自旋狀態(詳細過程看下面的原始碼解析),再迴圈過程中還會將標識為取消的前驅節點移除佇列,同時標識前驅節點狀態為SIGNAL。 - 執行緒的等待狀態是通過呼叫
LockSupport.lock()
方法實現的,這個方法會響應Thread.interrupt
,但是不會丟擲InterruptedException異常,這點與Thread.sleep
、Thread.wait
不一樣。
可以看到節點和節點之間在自旋過程中除了前驅節點會喚醒該節點之外基本不會互相通訊
原始碼分析:
public final void acquire(int arg) {
//該執行緒呼叫tryAcquire方法嘗試以獨佔模式獲取資源,如果獲取失敗,則調
//用addWaiter函式,將執行緒封裝到Node節點中,然後再將Node節點加入到同
//步佇列的尾部,然後再呼叫acquireQueued讓執行緒進入到阻塞狀態,如果獲
//取成功則返回true,然後呼叫selfInterrupt
//函式。
//注意的是,tryAcquire函式就是繼承AQS的子類所需要去重寫的方法。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
//AQS的tryAcquire函式並沒有獲取資源的相關實現,需要繼承`AQS`的子類去
//重寫這個方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
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;
//CAS操作將新節點插入到,成功則返回,不成功則繼續下面的enq方法,
//進行死迴圈CAS插入,直到成功。
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果上面的CAS操作插入不成功,則呼叫enq方法 死迴圈插入 直到成功。
enq(node);
return node;
}
private Node enq(final Node node) {
//死迴圈 直到插入成功。
for (;;) {
Node t = tail;
//如果尾節點為null,說明同步佇列還未初始化,則CAS操作新建頭節點
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//通過CAS操作將節點插入到同步佇列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//節點以“死迴圈”的方式去獲取資源,為什麼死迴圈加了雙引號呢?因為迴圈並不
//是一直讓節點無間斷的去獲取資源,節點會經歷 獲取資源->失敗->執行緒進入等待
//狀態->喚醒->獲取資源......,執行緒在死迴圈的過程會不斷等待和喚醒,即節點的自旋。
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;
}
//呼叫shouldParkAfterFailedAcquire函式,將該節點的前驅節點
//的狀態設定為SIGNAL,告訴前驅節點我要去“睡覺”了,當資源排
//到你的時候,你就通知我一下讓我醒來,即節點做進入等待狀態
//的準備。
//當節點做好了進入等待狀態的準備,則呼叫parkAndCheckInterrupt
//函式,讓該節點進入到等待狀態。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//獲取前驅節點的狀態。
int ws = pred.waitStatus;
//如果前驅節點的狀態已經為SIGNAL了,即已經做好準備了,那直接返回。
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//如果前驅節點的狀態為取消狀態,則將前驅節點移除佇列,迴圈這個過程
//直到前驅節點不為取消狀態為止。
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;
//如果前驅節點沒有做好準備(標誌狀態為SIGNAL)、前驅節點也沒有被取消,
//則使用CAS操作將前驅節點的狀態更新為SIGNAL,然後返回false,為什麼
//是返回false呢?因為CAS操作並不保證一定能更新成功,返回false的目的
//是讓acquireQueued函式再執行一次for迴圈,這個迴圈第一可以讓該節點
//再嘗試獲取資源(萬一成功了呢 是吧),第二是讓acquireQueued函式再呼叫
//一次shouldParkAfterFailedAcquire函式(即本函式)判斷節點的前驅節點是
//否已經設定為SIGNAL狀態了。
} 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//呼叫LockSupport.park函式將該執行緒設定為等待狀態
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//注意LockSupport遇到Thread.interrupt是會立刻返回的,但是不會丟擲異常InterruptedExcept
//ion,這個需要注意和Thread.wait,Thread.sleep的區別,
//喚醒的時候 會返回該執行緒是否為中斷喚醒的。
return Thread.interrupted();
}
複製程式碼
釋放資源
- 執行緒呼叫子類重寫的
tryRelease
方法進行釋放資源,如果釋放成功則繼續檢查執行緒(節點)的是否有後繼節點,有後繼幾點則去喚醒。 - 呼叫
unparkSuccessor
方法進行後繼節點的喚醒,如果後繼節點為取消狀態,則從佇列的隊尾往前遍歷,找到一個離節點最近且不為取消狀態的節點進行喚醒,如果後繼節點不為取消狀態則直接喚醒。
public final boolean release(int arg) {
//執行緒呼叫tryRelease方法嘗試釋放資源,如果釋放成功則檢查該節點是否有後繼節點,有的話則
//呼叫unpacrkSuccessor()方法去喚醒後繼節點。
//注意的是,tryRelease函式就是繼承AQS的子類所需要去重寫的方法。
if (tryRelease(arg)) {
Node h = head;
//頭結點(即釋放資源的節點)不為空,頭結點的狀態不為0,代表有後繼節點,需要喚醒。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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;
//如果狀態小於0,即代表有後繼節點需要喚醒。
if (ws < 0)
//將頭結點的狀態置為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) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果頭結點的後繼節點不為取消狀態,則直接將後繼節點喚醒
if (s != null)
LockSupport.unpark(s.thread);
}
複製程式碼
共享模式
共享模式下,執行緒無論是獲取資源還是釋放資源,都可能會喚醒後繼節點。
獲取資源
- 呼叫子類重寫的
tryAcquireShared
方法進行資源獲取,獲取失敗則呼叫doAcquireShared
將執行緒封裝Node節點加入到同步佇列隊尾。 - 呼叫
doAcquireShared
方法讓節點以"死迴圈"方式進行獲取資源,為什麼死迴圈加了雙引號呢?因為迴圈並不是一直讓節點無間斷的去獲取資源,節點會經歷獲取資源->失敗->執行緒進入等待狀態->喚醒->獲取資源......,執行緒在死迴圈的過程會不斷等待和喚醒,節點進入到自旋狀態(詳細過程看下面的原始碼解析)。如果執行緒節點被喚醒後,且獲取資源成功,且後繼節點為共享模式,那麼會喚醒後繼節點......喚醒會一直傳遞下去,直到後繼節點不是共享模式,喚醒的節點同樣會去獲取資源,這點和獨佔模式不一樣。
public final void acquireShared(int arg) {
//和獨佔模式的一樣,同樣是呼叫子類重寫的tryAcquireShared方法以共享模式進行資源獲取。
//如果獲取失敗,則呼叫doAcquireShared方法將執行緒封裝成Node節點加入到同步佇列的隊尾,
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 failed = true;
try {
boolean interrupted = false;
for (;;) {
//獲取執行緒(節點)的前驅節點。
final Node p = node.predecessor();
//如果前驅節點為頭結點,則該執行緒嘗試獲取資源。
if (p == head) {
//獲取資源。
int r = tryAcquireShared(arg);
//獲取資源成功則將節點設為頭結點。
if (r >= 0) {
//獲取成功 對後繼SHARED節點持續喚醒
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//和獨佔模式的一樣。
//呼叫shouldParkAfterFailedAcquire函式,將該節點的前驅節點
//的狀態設定為SIGNAL,告訴前驅節點我要去“睡覺”了,當資源排
//到你的時候,你就通知我一下讓我醒來,即節點做進入等待狀態的準備。
//當節點做好了進入等待狀態的準備,則呼叫parkAndCheckInterrupt
//函式,讓該節點進入到等待狀態。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果節點為共享節點,則呼叫doReleaseShared函式喚醒後繼節點。
if (s == null || s.isShared())
doReleaseShared();
}
}
複製程式碼
釋放資源
- 呼叫子類重寫的
tryReleaseShared
方法釋放資源,釋放成功則呼叫doReleaseShared
方法進行後繼節點的喚醒。 - 如果後繼節點為共享模式,則持續喚醒。
共享模式下資源釋放流程和獨佔模式下資源釋放的流程差不多,就是在釋放後喚醒後繼為共享模式的節點,且喚醒的動作是傳播下去的,直到後繼節點出現不是共享模式的,這個喚醒的過程和共享模式的獲取資源的喚醒過程一樣。
//呼叫子類重寫的tryReleaseShared方法進行以共享模式釋放資源,釋放失敗則呼叫doReleaseShared。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果節點標識後繼節點需要喚醒,則呼叫unparkSuccessor方法進行喚醒。
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
複製程式碼
條件佇列
條件佇列又稱等待佇列、條件佇列等,條件佇列的實現是通過ConditionObject
的內之類來完成的,,一開始就介紹了同步佇列條件佇列的去,不過這裡再囉嗦一下,可以把同步隊和條件佇列理解成儲存等待狀態的執行緒的佇列,條件佇列中的執行緒並不能直接去獲取資源,而要先從條件佇列轉到同步佇列中排隊獲取,一個執行緒要麼是在同步佇列中,要麼是在條件佇列中,不可能同時存在這兩個佇列裡面。
/*
* 使當前執行緒進入等待狀態,直到以下4種情況任意一個發生:
* 1.另一個執行緒呼叫該物件的signal(),當前執行緒恰好是被選中的喚醒執行緒
* 2.另一個執行緒呼叫該物件的signalAll()
* 3.另一個執行緒interrupt當前執行緒(此時會丟擲InterruptedException)
* 4.虛假喚醒(源自作業系統,發生概率低)
* ConditionObject要求呼叫時該執行緒已經拿到了其外部AQS類的排它鎖(acquire成功)
*/
void await() throws InterruptedException;
/*
* 與await()相同,但是不會被interrupt喚醒
*/
void awaitUninterruptibly();
/*
* 與await()相同,增加了超時時間,超過超時時間也會停止等待
* 三個方法功能相似,其返回值代表剩餘的超時時間,或是否超時
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
/*
* 喚醒一個正在等待該條件變數物件的執行緒
* ConditionObject會選擇等待時間最長的執行緒來喚醒
* ConditionObject要求呼叫時該執行緒已經拿到了其外部AQS類的排它鎖(acquire成功)
*/
void signal();
/*
* 喚醒所有正在等待該條件變數物件的執行緒
* ConditionObject要求呼叫時該執行緒已經拿到了其外部AQS類的排它鎖(acquire成功)
*/
void signalAll();
複製程式碼
可以看到,其作用與Object原生的wait()/notify()/notifyAll()很相似,但是增加了更多的功能。下面以awaitUninterruptibly()、signal()為例,闡述一下其內部實現。
同步佇列和條件佇列的關係
執行緒執行condition.await()
方法,將節點1從同步佇列轉移到條件佇列中。
執行緒執行condition.signal()
方法,將節點1從條件佇列中轉移到同步佇列。
參考
簡述AbstractQueuedSynchronizer
一行一行原始碼分析清楚AbstractQueuedSynchronizer
Java併發包原始碼學習之AQS框架(四)AbstractQueuedSynchronizer原始碼分析
【Java併發】詳解 AbstractQueuedSynchronizer
AbstractQueuedSynchronizer的介紹和原理分析
原文地址:ddnd.cn/2019/03/15/…