隊列同步器(AQS)簡介:
AbstractQueueSynchronizer,用來構建鎖和其他同步組件的基礎框架,使用一個int型變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
我們可以這麼理解,鎖是面向使用者的,即我們可以用鎖來完成多線程處理的一些問題,而隱藏了實現的細節,而同步器面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理,線程派對,等待與喚醒等底層的操作。相當於使用者使用鎖,而AQS來實現鎖。
使用場景:比如可重入鎖,CountDownLatch等鎖和工具類都用到了AQS
使用方式:
子類繼承並實現他的抽象方法來管理同步狀態,對同步狀態進行修改時,使用同步器提供的3個方法(getState()、setState()、compareAndSetState())來操作,他們能保證狀態的改變是安全的。子類推薦爲自定義同步組件的靜態內部類。
剛纔提到的三個方法getState()、setState()、compareAndSetState()都是final方法,我們並不需要去重寫,需要重寫的方法是下面的這幾個方法:
當我們自定義同步組件時,將會調用同步器提供的模板方法,這些模板方法如下:
這些模板方法同樣是final的,我們在調用他們時,也會調用到我們之前重寫的方法,在後面我們會介紹到這些模板方法。
下面給出一個例子,大概瞭解一下怎麼使用(靜態內部類繼承AQS)
class Mutex implements Lock {
// 靜態內部類, 自定義同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否處於佔用狀態
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 當狀態爲0的時候獲取鎖
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1) ) {
setExclusiveOwnerThread(Thread. currentThread() ) ;
return true;
}
return false;
}
// 釋放鎖, 將狀態設置爲0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new
IllegalMonitorStateException() ;
setExclusiveOwnerThread(null) ;
setState(0) ;
return true;
}
// 返回一個Condition, 每個condition都包含了一個condition隊列
Condition newCondition() { return new ConditionObject() ; }
}
// 僅需要將操作代理到Sync上即可
private final Sync sync = new Sync() ;
public void lock() { sync. acquire(1) ; }
public boolean tryLock() { return sync.tryAcquire(1) ; }
public void unlock() { sync. release(1) ; }
public Condition newCondition() { return sync.newCondition() ; }
public boolean isLocked() { return sync. isHeldExclusively() ; }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads() ; }
public void lockInterruptibly() throws InterruptedException {
sync. acquireInterruptibly(1) ;
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync. tryAcquireNanos(1, unit.toNanos(timeout) ) ;
}
}
同步隊列:
AQS中很重要的一個數據結構就是同步隊列了,他是一個FIFO雙向的同步隊列。
當前的線程在獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
Node節點是AQS中的一個內部類,成員如下
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
而每個同步器中都會持有同步隊列的首節點和尾節點
private transient volatile Node head;
private transient volatile Node tail;
所以基本結構如下:
剛纔說了獲取同步狀態失敗時,就會把線程信息加入一個新構建的節點,然後接入隊列的尾部,所以這個加入隊列的過程也必須要保證線程的安全,所以同步器有一個基於CAS的設置尾節點的方法:
compareAndSetTail(Node except,Node update)
首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,就會喚醒後續的節點,而後續的節點將會在獲取同步狀態成功時將自己設置爲首節點。設置首節點是由獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功的獲取到同步狀態,因此就不需要CAS來保證線程安全了(就只有一個線程)。
同步狀態的獲取與釋放:
在瞭解了同步隊列的結構後,我們就可以來看看AQS到底是怎樣來進行同步狀態的獲取和釋放的。
獲取與釋放分爲獨佔式的和共享式的。
獨佔式:顧名思義就是同一時刻只能有一個線程獲取到鎖,其他獲取鎖線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取到鎖。
共享式:同一時刻能夠有多個線程獲取到同步狀態。
(1)獨佔式同步狀態的獲取:
通過acquire(int arg)方法獲取同步狀態,該方法對於中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中, 後續對線程進行中斷操作時, 線程不會從同步隊列中移出。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這個方法中調用了重寫之後tryAcquire(int arg)方法,還調用了addWaiter方法和acquireQueued方法
下面我們一個一個來分析一下這些方法
首先是addWaiter(Node node)方法
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;
}
這個方法會使用compareAndSetTail()方法來吧當前線程加入尾節點,如果沒有加入成功,就會去調用enq()方法,那我們再看看enq(final Node node)方法
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;
}
}
}
}
可以看出,enq方法就是一個死循環,而它所完成的工作和addWaiter(Node node)是一樣的,都是吧當前的線程加入到同步隊列的尾節點處,所以enq方法和addWaiter可以看做是同一個方法來對待,通過CAS設置尾節點的方式,將併發添加節點的請求變得串行化了,也就是保證了尾節點添加是線程安全的。在執行完這兩個方法之後,就會去調用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);
}
}
同理,這還是一個死循環的方法,它的邏輯是判斷前驅節點是不是首結點(這個時候通過之前說的方法當前線程已經成功的加入了同步隊列),假如是首節點(只有他的前驅是首節點他纔有機會獲取同步狀態)那就嘗試獲取同步狀態,獲取成功的話就把自己設置爲首節點,否則就繼續循環直到他獲取成功爲止。
仔細看看這個方法是個無限循環,感覺如果p == head && tryAcquire(arg)條件不滿足循環將永遠無法結束,當然不會出現死循環,奧祕在於後面的parkAndCheckInterrupt會把當前線程掛起,從而阻塞住線程的調用棧。當然也不是馬上把請求不到鎖的線程進行阻塞,還要檢查該線程的狀態,比如如果該線程處於Cancel狀態則沒有必要,具體的檢查在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;
}
檢查原則在於:
- 規則1:如果前繼的節點狀態爲SIGNAL,表明當前節點需要unpark,則返回成功,此時acquireQueued方法的第12行(parkAndCheckInterrupt)將導致線程阻塞
- 規則2:如果前繼節點狀態爲CANCELLED(ws>0),說明前置節點已經被放棄,則回溯到一個非取消的前繼節點,返回false,acquireQueued方法的無限循環將遞歸調用該方法,直至規則1返回true,導致線程阻塞
- 規則3:如果前繼節點狀態爲非SIGNAL、非CANCELLED,則設置前繼的狀態爲SIGNAL,返回false後進入acquireQueued的無限循環,與規則2同
那麼我們現在總結一下,介紹了剛纔那麼多的方法,大多數的方法裏面都有一個死循環,這時回到最初的acquire(int arg)方法,我們就大概能夠明白這個方法的邏輯了,當獲取失敗時,那麼後面的方法就會把當前線程加入同步隊列,並且讓該節點一直自旋(相當於被阻塞了),直到他獲取到了同步狀態成功(別的線程釋放了)爲止。
那麼現在還有一個疑問,假如第一個獲取的線程在tryAcquire方法中就獲取同步狀態成功,直接返回ture,那麼線程就不會被包裝成節點加入到同步隊列中,那隊列何來的首節點和尾節點?
其實我們回去看addWaiter和enq方法就會發現這些方法都會去判斷是否有尾節點,當發現沒有尾節點時,就會在enq方法裏就會創建首節點和尾節點(首節點就是尾節點)
(代碼片段)
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
說了這麼多,對於鎖這種併發組件來說的話,從acquire(int arg)這個方法返回就代表當前線程獲取了鎖,以上就是這個方法的含義。
獲取的邏輯圖:
(2)獨佔式同步狀態的釋放
剛纔講的是獲取,現在講的是如何釋放同步狀態。通過調用AQS的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;
}
ps:喚醒的方法unparkSuccessor(Node node)使用了LockSupport工具類
(3)共享式同步狀態的獲取與釋放:
以文件的讀寫爲例,寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。
一個線程在讀時,其他線程也可以讀,但是一個線程在寫時,其他線程均不能讀寫。
調用方法acquireShare(int arg)可以共享式獲取同步狀態,
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
當tryAcquireShared返回值大於等於0時,表示可以獲取到同步狀態,否則進入doAcquireShared(int arg)方法中自旋
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);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同樣是一個死循環,不斷的進行tryAcquireShare(int arg)方法,直到獲取成功,其實它和獨佔式的差別不大,差別主要在setHeadAndPropagate方法,顧名思義,即在設置head之後多執行了一步propagate操作
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 = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
這意味着獨佔鎖某個節點被喚醒之後,它只需要將這個節點設置成head就完事了,而共享鎖不一樣,某個節點被設置爲head之後,如果它的後繼節點是SHARED狀態的,那麼將繼續通過doReleaseShared方法嘗試往後喚醒節點,實現了共享狀態的向後傳播。
釋放同步狀態使用releaseShare(int arg)方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的, 因爲釋放同步狀態的操作會同時來自多個線程。
(4)獨佔式超時獲取同步狀態
先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改, 但線程依舊會阻塞在synchronized上,等待着獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法, 這個方法在等待獲取同步狀態時, 如果當前線程被中斷, 會立刻返回, 並拋出InterruptedException。超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上, 增加了超時獲取的特性。 針對超時獲取, 主要需要計算出需要睡眠的時間間隔nanosTimeout, 爲了防止過早通知,nanosTimeout計算公式爲: nanosTimeout-=now-lastTime, 其中now爲當前喚醒時間, lastTime爲上次喚醒時間, 如果nanosTimeout大於0則表示超時時間未到, 需要繼續睡眠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);
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);
}
}
該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object blocker,long nanos)方法返回)。如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自 旋過程。 原因在於,非常短的超時等待無法做到十分精確, 如果這時再進行超時等待, 相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下, 同步器會進入無條件的快速自旋。
使用的例子
最後給出一個使用的例子——TwinsLock,這個工具類允許在同一時刻,之多兩個線程同時訪問。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2) ;
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count) ;
}
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState() ;
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current,
newCount) ) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState() ;
int newCount = current + returnCount;
if (compareAndSetState(current, newCount) ) {
return true;
}
}
}
}
public void lock() {
sync. acquireShared(1) ;
}
public void unlock() {
sync. releaseShared(1) ;
}
// 其他接口方法略
}