AbstractQueuedSynchronizer
所謂AQS,指的是AbstractQueuedSynchronizer,它提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等併發類均是基於AQS來實現的,具體用法是通過繼承AQS實現其模板方法,然後將子類作爲同步組件的內部類。
變量waitStatus則表示當前Node結點的等待狀態,共有5種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示當前結點已取消調度。當timeout或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的結點將不會再變化。
- SIGNAL(-1):表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲SIGNAL。
- CONDITION(-2):表示結點等待在Condition上,當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
- PROPAGATE(-3):共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。
- 0:新結點入隊時的默認狀態。
AQS內部使用CLH算法維護等待隊列,CLH鎖即Craig, Landin, and Hagersten (CLH) locks。CLH鎖是一個自旋鎖。能確保無飢餓性。提供先來先服務的公平性。
CLH鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋。
CLH隊列中的結點QNode中含有一個locked字段,該字段若爲true表示該線程須要獲取鎖,且不釋放鎖。爲false表示線程釋放了鎖。
結點之間是通過隱形的鏈表相連,之所以叫隱形的鏈表是由於這些結點之間沒有明顯的next指針,而是通過myPred所指向的結點的變化情況來影響myNode的行爲。
CLHLock上另一個尾指針,始終指向隊列的最後一個結點。
CLHLock的類圖例如以下所看到的:
當一個線程須要獲取鎖時,會創建一個新的QNode。將當中的locked設置爲true表示須要獲取鎖。然後線程對tail域調用getAndSet方法,使自己成爲隊列的尾部。同一時候獲取一個指向其前趨的引用myPred,然後該線程就在前趨結點的locked字段上旋轉。直到前趨結點釋放鎖。
當一個線程須要釋放鎖時,將當前結點的locked域設置爲false。同一時候回收前趨結點。例如以下圖所看到的,線程A須要獲取鎖。其myNode域爲true。些時tail指向線程A的結點,然後線程B也增加到線程A後面。tail指向線程B的結點。然後線程A和B都在它的myPred域上旋轉,一量它的myPred結點的locked字段變爲false,它就能夠獲取鎖掃行。明顯線程A的myPred locked域爲false,此時線程A獲取到了鎖。
獲取獨佔鎖:
AQS實現類中類似於ReentrantLock類,在獲取鎖的操作中最後調用的是如下方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法由具體實現類實現,顧名思義用來獲取資源,在第一次獲取資源失敗後進入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
首先看下addWaiter(Node.EXCLUSIVE):
private Node addWaiter(Node mode) {
//創建獨佔模式的節點
Node node = new Node(mode);
//自旋插入節點
for (;;) {
//獲取尾部節點
Node oldTail = tail;
//如果尾部節點不爲空
if (oldTail != null) {
//新節點的前置節點設置爲原先的尾節點
node.setPrevRelaxed(oldTail);
//將新節點設置爲尾節點
if (compareAndSetTail(oldTail, node)) {
//原先尾節點next節點設置爲新節點
oldTail.next = node;
return node;
}
} else {
//如果沒有尾節點則說明隊列爲空需要初始化
initializeSyncQueue();
}
}
}
如上爲在等待隊列中插入當前獲取資源的節點。
接下來看acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
//自旋
for (;;) {
//獲取當前節點的前置節點
final Node p = node.predecessor();
//如果前置節點爲頭節點,說明馬上就輪到自己了,可以先嚐試獲取資源
//在頭結點釋放資源後並喚醒下一個節點的時候p==head成立,進入循環
//這裏保證FIFO
if (p == head && tryAcquire(arg)) {
//如果獲取成功,將當前節點設置爲頭節點
setHead(node);
p.next = null; // help GC
return interrupted;
}
//保證前置節點狀態爲SIGNAL
if (shouldParkAfterFailedAcquire(p, node))
//調用park方法將自己阻塞,激活後檢查中斷狀態並與interrupted或運算
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
再看下shouldParkAfterFailedAcquire(p, node)方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//獲取前繼節點的waitStatus
int ws = pred.waitStatus;
//如果ws爲SIGNAL狀態,表面前繼節點釋放資源或中斷後會喚醒自己直接返回true
if (ws == Node.SIGNAL)
return true;
//如果ws>0即爲取消狀態,跳過此前置節點,一直往前找,直到找到waitStatus<0的節點,並將此節點設置爲 //node的前置節點,同時設置此節點的後置節點爲node
if (ws > 0) {
do {
//這裏往前變量才能保證if (p == head && tryAcquire(arg)) 跳出循環
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 將前繼節點的ws值設置爲Node.SIGNAL,以保證下次自旋時,shouldParkAfterFailedAcquire直接返回true
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
釋放獨佔鎖:
AQS實現類中類似於ReentrantLock類,在釋放鎖的操作中最後調用的是如下方法。
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方法由具體實現類實現,顧名思義用來釋放資源,釋放成功進入後面代碼。
接下來看unparkSuccessor方法:
private void unparkSuccessor(Node node) {
//獲取頭結點waitStatus
int ws = node.waitStatus;
if (ws < 0)
//如果ws小於0,將其waitStatus置爲0
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
//如果頭結點的nest節點爲空或者waitStatus大於0,則從尾節點從後往前查找
//搜索到等待隊列中最靠前的ws值非正且非null的節點
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);
}
後繼節點的阻塞線程被喚醒後,就進入到acquireQueued()的if (p == head && tryAcquire(arg))的判斷中,此時被喚醒的線程將嘗試獲取資源。
當然,如果被喚醒的線程所在節點的前繼節點不是頭結點,經過shouldParkAfterFailedAcquire的調整,也會移動到等待隊列的前面,直到其前繼節點爲頭結點。
獲取共享鎖
AQS實現類中類似於ReentrantReadWriteLock類,在獲取共享鎖的操作中最後調用的是如下方法。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared方法爲AQS集成類實現,獲取失敗後進入doAcquireShared方法:
private void doAcquireShared(int arg) {
//此addWaiter與獲取獨佔鎖中的一致,將一個共享節點放入等待隊列隊尾
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
//自旋操作
for (;;) {
//獲取新節點前驅節點
final Node p = node.predecessor();
if (p == head) {
//如果p就是頭結點進入下面邏輯
//嘗試獲取資源
int r = tryAcquireShared(arg);
if (r >= 0) {
//獲取成功後將當前節點設爲頭結點,如果r>0並喚醒後繼節點
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
//保證前置節點狀態爲SIGNAL,方法詳情分析見獨佔鎖中分析
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
上述代碼中與獲取獨佔鎖邏輯大致一樣,不同的地方在於setHeadAndPropagate方法,跟蹤代碼如下:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//將當前節點設爲頭節點
setHead(node);
//當前節點獲取成功後返回值大於0,表面後繼節點有可能獲取資源成功,所以需要喚醒後繼節點
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
喚醒後繼節點的方法爲doReleaseShared()代碼如下:
private void doReleaseShared() {
//自旋進行喚醒,由於多個線程在操作故每次獲取的頭結點可能會變,操作完頭結點沒變則跳出循環
for (;;) {
//獲取頭結點
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//ws爲SIGNAL則將狀態置爲0
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
//喚醒後繼節點
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
釋放共享鎖:
AQS實現類中類似於ReentrantReadWriteLock類,在釋放共享鎖的操作中最後調用的是如下方法。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared由實現類實現,doReleaseShared上面已經分析過了。
至此,共享模式下的資源獲取/釋放就講解完了,下面以一個具體場景來概括一下:
整個獲取/釋放資源的過程是通過傳播完成的,如最開始有10個資源,線程A、B、C分別需要5、4、3個資源。
- A線程獲取到5個資源,其發現資源還剩餘5個,則喚醒B線程;
- B線程獲取到4個資源,其發現資源還剩餘1個,喚醒C線程;
- C線程嘗試取3個資源,但發現只有1個資源,繼續阻塞;
- A線程釋放1個資源,其發現資源還剩餘2個,故喚醒C線程;
- C線程嘗試取3個資源,但發現只有2個資源,繼續阻塞;
- B線程釋放2個資源,其發現資源還剩餘4個,喚醒C線程;
- C線程獲取3個資源,其發現資源還剩1個,繼續喚醒後續等待的D線程;
- …
參考文章:https://www.jianshu.com/p/0f876ead2846
https://blog.csdn.net/varyall/article/details/80317488