AbstractQueuedSynchronizer簡介
AbstractQueuedSynchronizer(AQS)是實現依賴於FIFO等待隊列的阻塞鎖定或者相關同步器(ReentrantLock, Semaphore等)的一個框架,在java.util.concurrent併發庫中有着舉足輕重的作用。AQS把單個原子int值來表示狀態(對於long有專屬類AbstractQueuedLongSynchronizer),一般子類通過繼承該類後,定義哪種狀態屬於當前對象被獲取或者被釋放,一般使用getState()、setState(int)、compareAndSetState(int, int)方法來更新int值。
AQS支持獨佔(exclusive)模式或者共享(shared)模式。在獨佔模式下,只有指定線程可以成功獲得鎖。在共享模式下,多個線程可以獲取鎖成功。
AQS內部定義一個嵌套類ConditionObject,可以用做Condition實現。
一般情況下,子類爲來實現對應的同步需求,可以重載以下方法(具體原理稍後解釋)tryAcquire(int) 試圖在獨佔模式下獲取對象狀態。此方法應該查詢是否允許它在獨佔模式下獲取對象狀態,如果允許,則獲取它。
tryRelease(int) 試圖設置狀態來反映獨佔模式下的一個釋放。
tryAcquireShared(int) 試圖在共享模式下獲取對象狀態。此方法應該查詢是否允許它在共享模式下獲取對象狀態,如果允許,則獲取它。
tryReleaseShared(int) 試圖設置狀態來反映共享模式下的一個釋放。
isHeldExclusively() 如果對於當前(正調用的)線程,同步是以獨佔方式進行的,則返回 true。
具體實現解析
1. AQS等待隊列原理
AQS內部以一個雙向鏈表隊列作爲線程等待隊列(CLH鎖隊列變種),每個等待這把鎖的線程作爲鏈表的一個結點。每個結點包含一個“status”域來跟蹤每條線程是否需要阻塞。當祖先被釋放的時候,這個結點會被觸發。在等待隊列中的處於第一個的線程可以嘗試獲取鎖,但處於第一不保證一定成功獲取,只代表它有權去競爭,所以當前釋放的競爭者線程可能需要重新等待。作爲一個FIFO等待隊列的實現,喚醒結點從head開始,插入結點從tail開始。
//等待隊列結構如下:
+------+ prev +-----+ +-----+
head | dummy| <---- | | <---- | | tail
| node | next | | | |
+------+ ----> +-----+ ----> +-----+
head一直只會引用一個無效空結點,其後繼結點纔是第一個表示等待該鎖的線程結點。
prev主要用在取消獲取鎖操作上。如果一個結點被取消來,他的後繼結點一般會被重新連接到一個非取消的祖先。
next用來實現阻塞機制。祖先通過遍歷next連接觸發下一個結點去決定被喚醒的線程。後繼結點的查找必須避免與新進入隊列的結點設置next域競爭。如果出現後繼結點爲null的情況下,可以通過從tail開始從後往前的查詢真正的繼承者解決競爭。(或者,next連接可以看到避免經常從後往前的掃描的優化)。
CLH隊列需要一個無效的頭結點作爲初始化。不過實現裏並沒有在構造時刻創建,因爲可能如果沒有產生競爭的時候,這個無效頭結點就浪費。因此是在第一次競爭的時候創建這個無效頭結點。
結點狀態有以下幾種static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
另外還包括默認值0。
SIGNAL 該結點的後繼者已經(或者很快就會)被阻塞,所以當前結點必須在釋放或者取消的時候喚醒它的後繼結點。
CANCELLED 當前結點由於超時或者interrupt被取消,一經設置,結點無法改變此狀態。換句話說,該線程不會再次被阻塞。
CONDITION 結點目前在結點隊列裏。當重新進入等待隊列後,這個狀態會被設置爲0。
PROGPAGATE 一個releaseShared應該被傳遞給其他結點。在doReleaseShared裏設置(只針對頭結點)去確保傳遞會繼續。這些狀態值裏,非負數表示一個結點不需要被觸發。所以,大部分代碼值需要簡單測試正負即可。
在條件狀態下的線程使用相同結點表達,不過使用裏額外的連接。由於條件狀態只能在獨佔模式下訪問,因此只需要簡單的結點連接(不存在併發)。在等待時候,一個結點會插入到條件隊列,在觸發的時候,這個結點會把轉移到等待隊列,一個特別的狀態值(CONDITION)會被設置去標記這個結點在哪個隊列上。2.源碼剖析
AQS作爲java併發的重中之中,內部實現相當巧妙,但千里之行始於足下,我們先從獨佔模式開始,從acquire入手剖析如果多個線程同時爭搶鎖的時候,爭搶失敗的線程如何進入隊列等待鎖。
(1)acquire獲取鎖流程首先,獲取鎖一般是acquire函數,不過AQS提供了acquire的多個變種,其中包括(僅限獨佔模式)
//在獨佔模式下獲取鎖,忽略線程interrupted
public final void acquire(int arg)
//在獨佔模式下獲取鎖,如果線程interrupted,拋出異常,放棄等待鎖。
public final void acquireInterruptibly(int arg)
//嘗試在獨佔模式下獲取鎖,如果線程interrupted,拋出異常,同時等待超過指定時間時,會返回失敗
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
事實上,三種變種的具體實現結構差不多,因此我們先集中剖析acquire獲取。函數源碼如下。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire由子類重新實現邏輯,一般子類可以通過判斷當前是否已經有線程獲取了鎖,或者判斷當前線程是否已經獲得了鎖(可重入)等等,通過判斷當前線程能否獲得鎖,返回了true,證明當前線程獲取了鎖,也就不需要競爭,會直接退出函數,如果返回了false,則需要把當前線程加入等待隊列。加入等待隊列是通過addWaiter實現,函數源碼如下
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;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
這裏,我們賦予參數mode爲Node.EXCLUSIVE,查看定義是一個null的Node,表達這是在獨佔模式。(在共享模式下,這是一個經過初始化的final空結點,用於區分獨佔結點)。把當前線程以及模式作爲參數構造一個新的Node之後,首先嚐試從tail結點插入,如果tail結點沒有初始化,或者產生了競爭的時候,就會調用enq函數利用循環CAS來保證從tail插入結點。
enq函數實現如下
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//head,tail初始化後:
+------+
head | dummy| tail
| node |
+------+
//添加第一個結點:
+------+ prev +-----+
head | dummy| <---- | | tail
| node | next | |
+------+ ----> +-----+
這是一個循環的CAS,循環內部會檢查tail,由於這裏是產生競爭纔會執行,所以tail爲null即head和tail還沒有進行初始化,於是把tail和head引用一個無效的空結點。當初始化完成後,就是典型的CAS把當前結點插入到tail,成功就會返回到上一步。注意,這時head引用的還是無效空結點,tail指向實際的結點。addWaiter把當前線程入隊後,就會到acquireQueued函數,代碼如下
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);
}
}
首先該函數主體也是一個循環和條件判斷來避免競爭帶來的問題。在循環內部,首先獲取調用函數predecessor獲取祖先結點,函數實現裏有一個null判斷prev,會拋出NullPointerException(註釋作用爲help the VM,個人感覺方便從異常裏查找問題)。然後判斷p == head是由於頭結點的直接後繼纔是第一個能夠重新競爭的線程,如果是頭結點的直接後繼結點,這裏再次重新判斷tryAcquire來嘗試獲取鎖,如果成功的話,則會調用setHead把head指向當前結點,然後返回。如果tryAcquire失敗,則會通過shouldParkAfterFailedAcquire把當前線程的waitStatus的狀態標識設置爲SIGNAL,然後返回false,再重新嘗試獲取鎖,如果再次失敗,則會進入parkAndCheckInerrupt,把當前線程進入阻塞狀態,直到release把當前線程喚醒,然後再次循環進行tryAcquire獲取鎖,除非成功退出或者失敗再次進入阻塞,這樣做可以避免多線程競爭鎖帶來的問題。
先看看shouldParkAfterFailedAcquire實現。
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 {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
傳入的參數是當前結點以及其前繼結點。由於默認的時候waitStatus爲0,因此第一次調用的時候會把waitStatus設置爲SIGNAL,如果waitStatus大於0(CANCELLED狀態),則會迭代查找前繼結點。返回true,則會在往後的操作裏進行阻塞狀態,如果返回false則會進入上一步的循環重新嘗試獲取鎖。因此這個函數主要在更改前繼結點狀態到SIGNAL。這樣做的目的,對比其更改到SIGNAL後直接返回true,是爲了避免多線程競爭的問題,這個競爭與下面釋放鎖有關,詳細例子見下面釋放鎖release的剖析。如果shouldParkAfterFailedAcquire返回true,則會執行parkAndCheckInterrupt,代碼如下
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport.park會把當前線程進行阻塞狀態。同時返回當前線程是否被interrupt。
另外,我們看看如果發生了異常退出循環,需要取消正在獲取鎖的行爲時,調用了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)) {
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 {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
首先,如果node爲null則不做任何事情直接return。然後,通過while循環判斷,直接前繼結點的waitStatus > 0(也就是CANCELLED)來跳過已經被取消來的前繼結點,然後predNext顯然就是需要取消鏈接的結點(通過CAS替換next域),但由於多線程競爭存在,因此下面的CAS更改可能會失敗,也就是說在與另外的cancel或者signal操作中競爭失敗,因此可以不需要另外的操作也不會影響。給node.waitStatus = Node.CANCELLED可以不需要CAS,這樣其它結點在判斷中就會跳過該結點。
接下來判斷,如果被取消的結點剛好是tail結點,並且CAS可以成功把tail結點替換成非取消的前繼結點pred,則同時利用CAS把pred的next域替換爲null。如果被取消的結點不是tail結點,則會再次判斷,如果該結點並非頭結點的直接後繼(因爲可能需要被喚醒)並且pred的後繼結點需要喚醒(也就waitStatus爲SIGNAL或者爲<=0),這樣就把被取消結點的next域賦給非取消前繼結點pred的next域。否則的話我們便喚醒被取消結點node的後繼結點,嘗試獲取鎖。
(2)release釋放鎖流程這樣大致瞭解來acquire獲取鎖的流程,我們來看看release釋放鎖究竟做了什麼。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
首先判斷tryRelease是否允許釋放鎖,一般子類重寫tryRelease會根據需要判斷能否釋放當前鎖中的等待隊列中的其他結點。當tryRelease返回true的時候,則會判斷head結點是否爲null並且head的waitStatus是否爲非0,如果判斷成功,則會調用unparkSuccessor來釋放後繼結點。
注意,這裏release鎖便與上面的獲取鎖先更改SIGANL,然後再返回循環重新獲取鎖的做法有競爭問題,我們先假設shouldParkAfterFailedAcquire函數更改了SIGNAL狀態後直接返回true,把當前線程進入阻塞,即 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
...
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
//更改了SIGNAL之後返回true,便把當前線程進入阻塞狀態
return true;
...
}
那麼當我們在獲取鎖的時候競爭失敗,進入了shouldParkAfterFailedAcquire之後,此時,release函數被調用,判斷waitStatus由於仍然沒改變SIGNAL,h.waitStatus == 0,然後就會退出函數,但此時shouldParkAfterFailedAcquire仍然執行,把當前線程阻塞了,這樣,就會由於併發問題進入死鎖狀態。因此,shouldParkAfterFailedAcquire採取的做法是先將狀態更改爲SIGNAL,然後返回false,這樣返回上一個堆棧裏的循環,就會還有一次tryAcquire的機會,這時當前線程便可以成功獲取了鎖,避免了死鎖狀態。我們回到release函數,判斷成功後,執行unparkSuccessor,代碼如下。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
函數首先判斷waitStatus,如果爲負,則變爲0,表明準備喚醒後繼結點,然後一般情況下後繼結點就是next域,但由於競爭存在,因此可能被cancelled或者爲null,因此需要從tail到head進行遍歷找出真正的沒有被cancelled的後繼結點。最後則調用LockSupport.unpark把線程喚醒,被喚醒的線程就會在acquireQueued的循環裏再次重新嘗試獲取鎖,這樣在循環裏重新獲取鎖的時候,由於有可能此時有外部線程調用acquire獲取鎖,這樣兩者競爭,如果失敗的話,則失敗的一方就會進入阻塞狀態。
這樣一來,acquire獲取鎖和release釋放鎖的流程大致清楚了,接下來看看其他變種的實現。(3)acquire的其它變種。
首先看看acquireInterruptibly變種。
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);
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);
}
}
可以看出,在acquireInterruptibly的開始添加了Thread.interrupted判斷後,拋出異常,以及在parkAndCheckInterrupt裏返回true拋出異常外,大體與acquire沒有區別。我們再來看看tryAcquireNanos變種。
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 {
long lastTime = System.nanoTime();
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;
}
if (nanosTimeout <= 0)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquireNanos裏同樣多了一個Thread.interrupted的判斷,而doAcquireNanos則稍微有點不一樣,除去一些超時的判斷之後,一個很關鍵的判斷nanosTimeout > spinForTimeoutThreshold,按照註釋,spinForTimeoutThreshold是一個大致的閥值,當小於這個閥值的時候,旋轉一次的時間比parkNanos會比更加快,此時選擇旋轉的話,會提高非常短時間的反應性,也就是說會更加精確達到超時控制。這樣,本次代碼剖析第一步就完結了,在這裏,我們先大致介紹了AQS的框架以及實現原理,然後從代碼上剖析了獲取鎖和釋放鎖的具體實現原理,從實現上可以看到對於多線程併發要注意的競爭問題。下一步將會進行關於共享模式的獲取鎖和釋放鎖的分析。