1 概述
上一篇文章 JUC--AQS源碼分析(二)同步狀態的獲取與釋放,我們學習到了同步狀態的獲取與釋放的源碼,並且對線程的阻塞和喚醒有了一個初步的瞭解,這裏我們進行深一步的分析。
2 阻塞
我們知道在獲取線程同步狀態失敗的時候,會將線程加入到CLH同步隊列,並且進行自旋等待。而在自旋等待方法acquireQueued中我們可以看見需要再次進行獲取同步狀態,如果獲取同步狀態失敗則需要判斷當前線程是否能夠被阻塞,並且進行線程阻塞後檢測中斷狀態。
//如果線程能夠被阻塞就阻塞線程並且返回線程的中斷狀態。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
接下來我們來學習shouldParkAfterFailedAcquire和parkAndCheckInterrupt着兩個方法到底是怎麼回事。
2.1 shouldParkAfterFailedAcquire判斷是否阻塞
首先直接上源碼,進行源碼分析。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前去節點狀態
int ws = pred.waitStatus;
//狀態爲SIGNAL,表示當前節點處於等待狀態,直接返回true。
if (ws == Node.SIGNAL)
return true;
//狀態大於0,則爲CANCEL,表示該節點已經超時,或者被中斷,需要CLH隊列中刪除。
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
//前驅節點狀態爲CONDITION、PROPAGATE
} else {
//CAS設置狀態爲SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上面這段代碼主要的作用就是檢查當前線程是否需要被阻塞,具體規則如下:
(1)如果前驅節點狀態爲SIGNAL,則表明當前線程需要被阻塞,則直接返回true。
(2)如果前驅節點狀態爲CANCEL,則表明該節點的前驅節點已經超時或者被中斷,需要從同步隊列中刪除,直到回溯到前驅節點的狀態<=0,返回false。
(3)如果前驅節點非SIGNAL,非CANCEL,則通過CAS方式將前驅節點設置成SIGNAL,返回false。
如果shouldParkAfterFailedAcquire返回true,則調用parkAndCheckInterrupt阻塞線程。
2.2 parkAndCheckInterrupt阻塞線程
首先還是直接上源碼。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
從上面的源碼我們可以看出parkAndCheckInterrupt做的事就相對簡單多了,直接阻塞當前線程,並且返回一個線程中斷狀態就行了。
3 喚醒
當線程釋放同步狀態後,就需要喚醒該線程的後繼節點。調用unparkSuccessor方法。
private void unparkSuccessor(Node node) {
//當前節點狀態
int ws = node.waitStatus;
//當前狀態 < 0 則設置爲 0,刪除當前節點
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//當前節點的後繼節點
Node s = node.next;
//後繼節點爲null或者其狀態 > 0 (超時或者被中斷了)
if (s == null || s.waitStatus > 0) {
s = null;
//從tail節點來找可用節點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//喚醒後繼節點
if (s != null)
LockSupport.unpark(s.thread);
}
爲何是從tail尾節點開始,而不是從node.next開始呢?由於node.next仍然可能會存在null或者取消了,所以採用tail回溯辦法找第一個可用的線程。
上面就是對AQS阻塞和喚醒線程的學習,後面用一篇博文學習LockSupport。