AbstractQueuedSynchronizer源碼解析
1.釋放鎖
釋放鎖是在顯式調用Lock.unlock()
方法時觸發的,目的是讓線程釋放對資源的訪問權。unlock方法的基礎方法是release
和releaseShared
,分別代表排它鎖和共享鎖的釋放。
釋放排它鎖—release方法
排它鎖的釋放過程比較簡單,分爲兩步
- 首先嚐試
tryRelease
方法釋放鎖,如果失敗會返回 false;反之,如果成功則執行步驟2 - 判斷同步隊列中是否還有節點在等待,如果有,此時當前節點一定是同步隊列的頭節點。調用
unparkedSuccessor
方法喚醒同步隊列中的一個後繼節點
具體源碼如下,
public final boolean release(int arg) {
// tryRelease 交給實現類去實現,一般就是用當前同步器狀態減去arg
// 如果返回 true 說明成功釋放鎖。
if (tryRelease(arg)) {
Node h = head;
// 說明見下方
if (h != null && h.waitStatus != 0)
// 從同步隊列頭開始喚醒等待鎖的節點
unparkSuccessor(h);
return true;
}
return false;
}
對上面的判斷if (h != null && h.waitStatus != 0)
進行說明,
3. 頭節點爲空說明同步隊列尚未初始化
4. 若頭節點的節點狀態爲0,說明是在enq
方法中compareAndSetHead(new Node())
初始化的頭節點。這種情況可能是其他節點加入同步隊列後,由於一些原因取消了,被從同步隊列中清除,導致隊列中只有一個waitStatus=0
的節點
大部分情況下,當節點指向的線程釋放鎖時,該節點應該是同步隊列的頭節點。但是也有特殊情況,這是由於acquire
方法中的操作造成的。上面兩種情況都說明,同步隊列是空的。這種情況下,無需進行後續節點的喚醒操作。
unparkSuccessor方法
unparkSuccessor
方法是的作用是喚醒同步隊列中下一個節點,
private void unparkSuccessor(Node node) {
// node節點是當前釋放鎖的節點,也是同步隊列的頭節點
int ws = node.waitStatus;
// 節點未被取消了,把節點的狀態置爲初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出 node 節點的後面一個節點
Node s = node.next;
// 判斷說明見下方
if (s == null || s.waitStatus > 0) {
s = null;
// 從同步隊列尾開始迭代,具體原因見下方
for (Node t = tail; t != null && t != node; t = t.prev)
// t.waitStatus <= 0 說明 t 沒有被取消,肯定還在等待被喚醒
if (t.waitStatus <= 0)
s = t;
}
// 喚醒最靠近同步隊列隊頭的狀態不爲CANCELLED的節點
if (s != null)
LockSupport.unpark(s.thread);
}
上方代碼中的判斷if (s == null || s.waitStatus > 0)
,
- s 爲空,表示 node 的後一個節點爲空
- s.waitStatus 大於0,代表 s 節點已經被取消了
遇到以上這兩種情況,就從隊尾開始,向前遍歷,找到最靠近隊頭的一個 waitStatus
字段不是被取消的節點對象。
尋找最靠近同步隊列頭部的狀態不爲cancelled
的節點的過程是從同步隊列尾部開始的,具體原因如下,
- 主要是因爲節點被阻塞的時候,是在
acquireQueued
方法的for循環中被阻塞的,喚醒時也一定會在acquireQueued
方法的for循環裏面被喚醒。 - 喚醒之後會進入新一輪循環,在循環中會判斷當前節點的前置節點是否是頭節點。
- 從尾到頭的迭代順序目的就是爲了過濾掉無效的前置節點,不然節點被喚醒時,發現其前置節點還是無效節點,就又會陷入阻塞。
acquireQueued
方法過程說明見博文。
獲取頭節點的後繼節點,當後繼節點的時候會調用LookSupport.unpark()方法
,該方法會喚醒該節點的後繼節點所包裝的線程。因此,每一次鎖釋放後就會喚醒隊列中該節點的後繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。
共享鎖釋放—releaseShared方法
釋放共享鎖分爲兩步,
- 首先嚐試
tryReleaseShared
釋放當前共享鎖,失敗會返回 false,如果成功釋放,則執行步驟2 - 調用
doReleaseShared
方法喚醒後繼節點
源碼如下,
// 共享模式下,釋放當前線程的共享鎖
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 這個方法就是線程在獲得鎖時,喚醒後續節點時調用的方法
doReleaseShared();
return true;
}
return false;
}
doReleaseShared方法
該方法跟獨佔式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對於能夠支持多個線程同時訪問的併發組件,必須保證多個線程能夠安全的釋放同步狀態,這裏採用的CAS保證,當CAS操作失敗continue,在下一次循環中進行重試。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
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;
}
if (h == head)
break;
}
}
2. 條件隊列重要方法
首先展示條件隊列的結構,單向鏈表,
之前synchronized
代碼中,利用Object的方式實際上是指在對象Object對象監視器上只能擁有一個同步隊列和一個等待隊列。而併發包中的Lock擁有一個同步隊列和多個條件隊列。示意圖如
ConditionObject是AQS的內部類,因此每個ConditionObject能夠訪問到AQS提供的方法,相當於每個Condition都擁有所屬同步器的引用。
進入條件隊列—await方法
當調用condition.await()
方法後會使得當前獲取lock的線程進入到條件隊列,如果該線程能夠從await()方法返回的話一定是該線程獲取了與condition相關聯的lock。
await方法源碼如下,
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 加入到條件隊列的隊尾
Node node = addConditionWaiter();
// 完全釋放所佔用資源,說明見下方
int savedState = fullyRelease(node);
int interruptMode = 0;
// 確認node不在同步隊列上,再阻塞,如果 node 在同步隊列上,是不能夠上鎖的
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)
// 如果狀態不是CONDITION,就會自動刪除
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法的具體流程,
- 調用
addConditionWaiter
方法將當前線程包裝成Node,插入到條件隊列的隊尾 - 釋放當前線程所佔用的lock,在釋放的過程中會喚醒同步隊列中的下一個節點
- 進入 while循環,判斷當前線程的節點不在同步隊列中,之後阻塞。阻塞是在 while循環中發生的,所以喚醒後也是在while循環中。
- 當節點被
signal
或signalAll
方法從條件隊列中調回同步隊列後會跳出while循環並調用acquireQueued
方法獲取lock
1)addConditionWaiter方法
該方法將線程包裝爲節點插入到條件隊列的隊尾,源碼如下,
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果尾部的 waiter 不是 CONDITION 狀態了,刪除
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新建條件隊列 node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 隊列是空的,直接放到隊列頭
if (t == null)
firstWaiter = node;
// 隊列不爲空,直接到隊列尾部
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
該方法返回的是包裝後的Node對象。
該方法中,在將節點接到隊尾之前會先判斷條件隊列尾節點的狀態,如果不是Node.CONDITION
就會調用unlinkCancelledWaiters
方法刪除條件隊列中所有狀態不爲Node.CONDITION
的節點。
同時可以看出條件隊列是一個不帶頭結點的鏈式隊列,之前學習AQS時知道同步隊列是一個帶頭結點的鏈式隊列,這是兩者的一個區別。
2)unlinkCancelledWaiters方法
該方法從條件隊列頭部開始,刪除掉所有狀態不對的節點,源碼如下,
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// trail 表示上一個狀態,這個字段作用非常大,可以把狀態都是 CONDITION 的 node 串聯起來,即使 node 之間有其他節點都可以
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 當前node的狀態不是CONDITION,刪除自己
if (t.waitStatus != Node.CONDITION) {
//刪除當前node
t.nextWaiter = null;
// 如果 trail 是空的,循環又是從頭開始的,說明從頭到當前節點的狀態都不是 CONDITION
// 都已經被刪除了,所以移動隊列頭節點到當前節點的下一個節點
if (trail == null)
firstWaiter = next;
// 如果找到上次狀態是CONDITION的節點的話,先把當前節點刪掉,然後把自己掛到上一個狀態是 CONDITION 的節點上
else
trail.nextWaiter = next;
// 遍歷結束,最後一次找到的CONDITION節點就是尾節點
if (next == null)
lastWaiter = trail;
}
// 狀態是 CONDITION 的 Node
else
trail = t;
// 繼續循環,循環順序從頭到尾
t = next;
}
}
流程示意圖如下,
3)fullyRelease方法
當前節點插入到等待對列之後,會使當前線程釋放lock,由fullyRelease
方法實現,源碼如下,
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
//成功釋放同步狀態
failed = false;
return savedState;
} else {
//不成功釋放同步狀態拋出異常
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
調用AQS的方法release
方法釋放AQS的同步狀態並且喚醒在同步隊列中頭結點的後繼節點引用的線程,如果釋放成功則正常返回,若失敗的話就拋出異常。
4)從await方法中退出
while (!isOnSyncQueue(node))
退出循環的條件是節點從條件隊列中取出放回至同步隊列。目前想到的只有兩種可能,
- node 剛被加入到條件隊列中,立馬就被其他線程 signal 轉移到同步隊列中去了
- 線程之前在條件隊列中沉睡,被喚醒後加入到同步隊列中去
退出循環後直接嘗試 acquireQueued
方法在同步隊列中阻塞直至獲取鎖。
線程喚醒
1)單個線程喚醒—signal方法
調用condition的signal或者signalAll方法可以將條件隊列中等待時間最長的節點移動到同步隊列中,使得該節點能夠有機會獲得lock。按照條件隊列是先進先出(FIFO)的,所以條件隊列的頭節點必然會是等待時間最長的節點,也就是每次調用condition的signal方法是將頭節點移動到同步隊列中。
該方法對應下圖的淺藍色曲線的過程,
signal
方法是ConditionObject類中的方法,源碼如下,
public final void signal() {
// 第一步檢驗
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 從頭節點開始喚醒
Node first = firstWaiter;
if (first != null)
// doSignal 方法會把條件隊列中的節點轉移到同步隊列中去
doSignal(first);
}
signal
方法首先會檢測調用signal
方法的線程是否已經獲取lock,如果沒有獲取lock會直接拋出異常,如果獲取的話再得到等待隊列的頭指針引用的節點- 之後的操作的
doSignal
方法也是基於該節點。
調用ConditionObject對象的signal
方法的前提條件是當前線程已經獲取了lock,該方法會使得條件隊列中的頭節點,即等待時間最長的那個節點移入到同步隊列。移入到同步隊列後纔有機會使得等待線程被喚醒,即從await方法中的LockSupport.park(this)
方法中返回,從而纔有機會使得調用await方法的線程成功退出。
條件隊列頭節點移入同步隊列—doSignal方法
該方法源碼如下,
private void doSignal(Node first) {
do {
// firstWaiter指向當前節點的nextWaiter
// 若firstWaiter爲空,說明到隊尾了
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 從隊列頭部開始喚醒
first.nextWaiter = null;
// transferForSignal 方法會把節點轉移到同步隊列中去
// (first = firstWaiter) = null 說明隊列中的元素已經循環完了
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal
方法將條件隊列頭節點的後置節點置爲 null,這種操作其實就是把 node 從條件隊列中移除。- 通過 while 保證
transferForSignal
方法能執行成功
喚醒過程最關鍵方法—transferForSignal
transferForSignal
方法會把節點轉移到同步隊列中去,該方法是真正對頭節點做處理的邏輯,源碼如下,
// 傳入參數 node 是條件隊列的頭節點
final boolean transferForSignal(Node node) {
// 將 node 的狀態從 CONDITION 修改成初始化,失敗返回 false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 當前隊列加入到同步隊列,返回的 p 是 node 加入同步隊列後的前置節點
Node p = enq(node);
int ws = p.waitStatus;
// 狀態修改成 SIGNAL,如果成功直接返回
// 把前置節點p的狀態改成 SIGNAL 是因爲 SIGNAL 本身就表示該節點後面的節點都是需要被喚醒的
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果 p 節點被取消,或者狀態不能修改成SIGNAL,直接喚醒
LockSupport.unpark(node.thread);
return true;
}
transferForSignal
方法返回 true 表示轉移成功, false 表示轉移失敗。
2)全部線程喚醒—signalAll方法
signalAll
方法的作用是喚醒條件隊列中全部節點,源碼如下,
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到頭節點
Node first = firstWaiter;
if (first != null)
// 從頭節點開始喚醒條件隊列中所有的節點
doSignalAll(first);
}
上方代碼與signal
方法的區別僅在於將內部調用的方法從doSignal
改爲doSignalAll
。
條件隊列所有節點移入同步隊列—doSignalAll方法
把條件隊列所有節點依次轉移到同步隊列去,
// 傳入參數 node 是條件隊列的頭節點
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
// 拿出條件隊列隊列頭節點的下一個節點
Node next = first.nextWaiter;
// 把頭節點從條件隊列中刪除
first.nextWaiter = null;
// 頭節點轉移到同步隊列中去
transferForSignal(first);
// 開始循環頭節點的下一個節點
first = next;
} while (first != null);
}
該方法將條件隊列中的每一個節點都移入到同步隊列中,即“通知”當前調用ConditionObject#await()
方法的每一個線程。
3. await與signal/signalAll的結合思考
圖示
使用 ConditionObject 提供的await
和signal/signalAll
方法就可以實現這種機制,而這種機制能夠解決最經典的問題就是“生產者與消費者問題”。
await和signal和signalAll方法就像一個開關控制着線程A(等待方)和線程B(通知方)。它們之間的關係可以用下面一個圖來表現,
- 線程awaitThread先通過
lock.lock()
方法獲取鎖成功後調用了condition.await
方法進入等待隊列 - 另一個線程signalThread通過
lock.lock()
方法獲取鎖成功後調用了condition.signal
或者signalAll
方法,使得線程awaitThread能夠有機會移入到同步隊列中 - 當其他線程釋放lock後使得線程awaitThread能夠有機會獲取lock,從而使得線程awaitThread能夠從await方法中退出執行後續操作。如果awaitThread獲取lock失敗會直接進入到同步隊列。
示例
public class AwaitSignal {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread waiter = new Thread(new waiter());
waiter.start();
Thread signaler = new Thread(new signaler());
signaler.start();
}
static class waiter implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + "當前條件不滿足等待");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "接收到通知條件滿足");
} finally {
lock.unlock();
}
}
}
static class signaler implements Runnable {
@Override
public void run() {
lock.lock();
try {
flag = true;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
}
上面代碼的執行結果,
Thread-0當前條件不滿足等待
Thread-0接收到通知,條件滿足