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。