AQS源码解析—Condition条件队列 await() & signal() 核心方法
简介
在 Condition 条件队列中使用的也是 AQS 中的 Node 结构,它并没有使用 prev 和 next 属性,而使用的是 nextWaiter 去实现了一个单向链表的结构:
Node nextWaiter;
流程图
条件队列流程图
await() 流程图
await() 节点出队重新构造入队流程图
signal() 流程图
signal() 节点迁移示意图
Condition 接口
- 下面将 AQS 的队列简称为
同步队列
,Condition 的队列(单向链表)简称为等待队列
。- 同步队列就是阻塞队列,等待队列就是条件队列。
public interface Condition {
// 阻塞 可以响应中断(抛出中断异常)
void await() throws InterruptedException;
// 阻塞 不响应中断
void awaitUninterruptibly();
// 线程超时等待,可以指定挂起时间,时间单位为纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 线程超时等待,可以指定挂起时间,时间单位可以自行设置
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 线程等待,直到收到被唤醒、或收到中断信号、或到了指定的截止时间日期自动唤醒
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒条件队列中第一个可以被唤醒的节点(将等待队列的节点迁移到同步队列中)
void signal();
// 唤醒条件队列中的所有节点全部迁移到同步队列中
void signalAll();
}
ConditionObject 实现类
/*
* Condition 接口的实现类
* 此类是AQS中的一个内部类,等待队列使用的节点也是AQS的内部类 Node
* 只不过与AQS的队列不同的是,等待队列中是单向链表。AQS的队列是双向链表。
* 注意:普通内部类是可以访问到类外部的域(后面要做节点的迁移)
*/
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 条件队列的第一个node节点(头节点)
private transient Node firstWaiter;
// 条件队列的最后一个node节点(尾节点)
private transient Node lastWaiter;
// lock.newCondition() 最终调用的还是这个构造器
// Lock => ReentrantLock => Sync => new Condition() => new ConditionObject()
public ConditionObject() { }
await() 方法
public final void await() throws InterruptedException {
// 判断当前线程是否是中断状态,如果是则直接抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
/*
* addConditionWaiter()将当前线程封装成Node节点加入条件队列中
* 并返回封装完成的Node节点(类似AQS中的addWaiter方法)
*/
Node node = addConditionWaiter();
/*
* fullyRelease() 完全释放当前线程的锁(state置为0)
* 因为这里要挂起等待被唤醒,所以必须先完全释放锁
*/
int savedState = fullyRelease(node);
/*
* 0 表示在Condition队列挂起期间没有接收过中断信号
* -1 表示在Condition队列挂起期间接收到了中断信号(THROW_IE)
* 1 表示在Condition队列挂起期间接未接收到中断信号,但是迁移到“阻塞(同步)队列AQS的队列”之后,接收过中断信号(REINTERRUPT)
*/
int interruptMode = 0;
/*
* isOnSyncQueue(node)
* true表示当前节点已经迁移到了同步队列中
* false表示当前节点还在等待队列中如果当前节点还在等待队列中,则需要继续挂起。
*/
while (!isOnSyncQueue(node)) {
// 当前节点还在等待队列中,继续被挂起
LockSupport.park(this);
/*
* 这里就是唤醒后判断是否是被中断唤醒的,然后执行响应的中断逻辑。
*
* checkInterruptWhileWaiting()就算在等待队列挂起期间,
* 线程发生了中断,对应的node也会被迁移到同步队列(参考transferAfterCancelledWait)
* 如果当前node被中断过,也直接break。
*
* 什么时候会被唤醒?
* 1.常规:外部线程获取锁后,调用了signal()方法,转移条件队列的头节点,到同步队列,当这个节点获取锁后,会被唤醒
* 2.转移到同步队列后,发现同步队列的前驱节点状态是取消状态,此时会直接唤醒当前节点
* 3.当前节点挂起期间,被外部线程使用中断唤醒..
*/
// 如果不为0,说明发生过中断了,也会被迁移到阻塞队列中,直接break。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/*
* 到这里 说明当前node已经被迁移到了同步队列中
*/
/*
* 条件1:acquireQueued(node, savedState)就是在同步队列中竞争锁的逻辑 savedState作用:恢复加锁状态 需要把之前释放锁的状态设置回去
* 返回true -> 表示在同步队列中被外部线程中断唤醒过
* 返回false -> 表示在同步队列中没有被外部线程中断唤醒过
* 条件2:interruptMode != THROW_IE 成立,表示node在等待队列未发生过中断
* 两个条件同时成立,表示node在同步队列发生过中断。
* 所以将interruptMode设置为REINTERRUPT(在同步队列发生过中断)
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 考虑下 node.nextWaiter != null,条件什么时候成立呢?
// 其实是node在条件队列内时如果被外部线程中断唤醒时,会加入到阻塞队列
// 但是并未设置 nextWaiter = null,这里需要做一个清理工作。
if (node.nextWaiter != null) // clean up if cancelled
// 清理等待队列内取消状态的节点
unlinkCancelledWaiters();
/*
* 条件成立:说明挂起期间发生过中断(1.条件队列的挂起 2.条件队列之外的挂起)
*/
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter()
/*
* 将当前线程封装成一个Node加入到等待队列的末尾。
* (会将等待队列中所有处于取消状态的节点全部出队)
* 调用await方法的线程 都是 持锁状态的,也就是说 addConditionWaiter 这里不存在并发!
*/
private Node addConditionWaiter() {
// 指向队尾
Node t = lastWaiter;
/*
* 条件1:t != null 成立:说明当前条件队列中,已经有node元素了
* 条件2:node 在条件队列中时,它的状态是CONDITION(-2)
* t.waitStatus != Node.CONDITION 成立:说明当前node发生中断了..处于取消状态CANCELLED(1)
* 此时当前线程在队列中做一次清除处理,将所有处于取消状态的node全部干掉。
*/
if (t != null && t.waitStatus != Node.CONDITION) {
// 清理条件队列中所有取消状态CANCELLED(1)的节点
unlinkCancelledWaiters();
// 更新t指向最新的尾结点(lastWaiter可能会更新 因为可能处于取消状态CANCELLED(1))
t = lastWaiter;
}
// 将当前线程封装为一个Node,Node状态(waitStatus)为Condition(-2)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 下面就是入队的过程(这里只有加锁的线程可以进来,所以不需要加锁)
// 条件成立:说明条件队列中没有任何元素,当前线程是第一个进入队列的元素
if (t == null)
// 让firstWaiter 指向当前node
firstWaiter = node;
//说明当前条件队列已经有其它node了 做追加操作
else
// 让nextWaiter 指向当前追加的node
t.nextWaiter = node;
// 更新队尾引用指向 当前node
lastWaiter = node;
// 最终返回构造的节点
return node;
}
fullyRelease()
/*
* 完全释放锁
*/
final int fullyRelease(Node node) {
// 完全释放锁是否成功,当failed失败时,说明当前线程是未持有锁调用 await方法的线程(错误写法)
// 假设失败,在finally代码块中 会将刚刚加入到 条件队列的 当前线程对应的node状态 修改为 取消状态
// 后继线程就会将 取消状态的节点给清理出去了
boolean failed = true;
try {
// 获取当前的state的值
int savedState = getState();
/*
* 全部释放掉state,变为初始状态0 这里正常情况下都会成功。
* (release方法见AQS源码解析系列博客)
*/
if (release(savedState)) {
// 失败标记设置为false,不会再走下面finally中 失败并设置节点状态为取消的逻辑
failed = false;
/*
* 返回当前线程释放的state值
* 为什么要返回savedState?
* 这里最终要返回加锁前的状态,因为执行到这时,构造出来的Node节点还在等待队列中,
* 当迁移到同步队列后,再次被唤醒,还需要进行获取锁释放锁的逻辑,所以这里需要记录下初始锁的状态并返回。
*/
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue()
/*
* 判断当前Node是否在同步队列中
*/
final boolean isOnSyncQueue(Node node) {
/*
* 条件1:node.waitStatus == Node.Condition(-2)条件成立,
* 说明当前Node一定在等待队列,因为signal方法迁移节点到同步队列前,会将node的状态设置为0
*
* 条件2:前置条件:node.waitStatus != Node.Condition
* 1.此时node.waitStatus 可能 = 0(表示当前节点已经被signal了 在不在阻塞队列另说 可能在迁移过程中)
* 2.此时node.waitStatus 可能 = 1(取消) (当前线程是未持有锁调用await方法,最终会将node的状态改为取消状态)
*
* node.waitStatuas == 0 为什么还要判断 node.prev == null?
* 因为signal是先修改状态 再迁移
* 因为等待队列的Node是单向链表,node.prev == null 说明一定是在等待队列中
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
// 直接return false
return false;
/*
* 执行到这里,会是哪种情况?
* node.waitStatus != Condition 且 node.prev != null
* 可以排除 node.waitStatus == 1 取消状态
* 为什么可以排除?因为signal不会把取消状态的node迁移走
*
* 设置prev引用的逻辑 是 迁移到同步队列设置的(enq()自旋入队)
* 入队的逻辑:
* 1.设置node.prev = tail;
* 2.CAS设置当前node为tail,成功才算真正进入到同步队列
* 3.pred.next = node;
* 可以推算出,就算prev不为null,也不能说明当前node已经成功入队到同步队列了(CAS成功才算)
*/
/*
* next不为null,说明当前节点已经成功入队到同步队列了,且当前节点后面已经有其他node了
*/
if (node.next != null)
return true;
/*
* 执行到这里,说明当前节点的状态为 node.witStatus = 0 && node.prev != null
* findNodeFromTail:从同步队列的尾巴开始向前遍历查找node,查找成功返回ture,失败返回false
* 当前node有可能在signal过程中,正在迁移中..还未完成
*/
return findNodeFromTail(node);
}
// 在阻塞队列中从后向前遍历查找元素
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
checkInterruptWhileWaiting()
/*
* 检查线程是在哪里被中断的 等待/同步(正在迁移过程中)
*/
private int checkInterruptWhileWaiting(Node node) {
/*
* Thread.interrupted():返回当前线程中断标记位,并且重置当前线程标记位为false
* transferAfterCancelledWait(node):如果当前线程被中断过,才会执行该方法,
* 被中断过就去判断是在哪里中被中断的,最终返回-1/1(-1表示在等待队列内,1表示在等待队列外(同步队列/迁移过程中))
* 没有被中断过直接返回0
*/
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
transferAfterCancelledWait()
/*
* @return true 表示node是在等待队列中唤醒的
* false 表示node不是在等待队列中唤醒的(同步队列/在迁移过程中)
*/
final boolean transferAfterCancelledWait(Node node) {
/*
* 条件成立:说明当前的node一定是在等待队列中被中断唤醒的
* CAS期望值为-2,修改为0,signal迁移节点到同步队列时,会将节点的状态修改为0
*/
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 在等待队列中 被中断唤醒的node也会被迁移到同步队列中
enq(node);
// 返回 true 表示node是在等待队列中被中断的
return true;
}
/*
* 执行到这里有几种情况?
* 1.当前node已经被外部线程调用signal 方法将其迁移到 同步队列了
* 2.当前node正在被外部线程调用signal 方法将其迁移到同步队列 进行中..
* (因为将node从等待队列迁移到同步队列时会先将状态改为0)
*/
while (!isOnSyncQueue(node))
// 如果当前线程状态被修改为0,但还未进入同步队列中,则短暂的释放CPU,再去抢占。
Thread.yield();
// 返回false这里表示node是在同步队列中被中断的(这里可能有些许歧义,可能此时还未进入同步队列,但该node一定不在等待队列中)
return false;
}
unlinkCancelledWaiters()
/*
* 将等待队列中的所有取消状态的node全部出队
*/
private void unlinkCancelledWaiters() {
// 循环当前节点,从链表的第一个节点开始向后迭代
Node t = firstWaiter;
// 当前链表上一个正常状态的node节点
Node trail = null;
// 遍历
while (t != null) {
Node next = t.nextWaiter;
// 当前节点t状态不正常(为取消状态)
if (t.waitStatus != Node.CONDITION) {
// 直接将nextWaiter置为null
t.nextWaiter = null;
// 条件成立:说明遍历到的节点还未碰到过任何正常节点
if (trail == null)
// 将firstWaiter指向next,即删除中间为取消状态的节点
firstWaiter = next;
// 执行到else这里,说明已经遇到过正常节点
else
// 让上一个正常节点指向 取消节点的 下一个节点,即跳过(删除)当前有问题的节点
trail.nextWaiter = next;
// 条件成立:遍历到链表的末尾了
if (next == null)
// 更新lastWaiter指向最后一个正常节点
lastWaiter = trail;
}
// 条件不成立执行到else,说明当前节点是正常节点
else
trail = t; // 更新一下trail
// 更新下一个节点
t = next;
}
}
reportInterruptAfterWait()
/*
* 根据interruptMode的值决定中断的处理方式
* 1.interruptMode = THROW_IE(-1):在条件队列内发生过中断
* 2.interruptMode = REINTERRUPT(1):在条件队列外发生过中断
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// 条件成立:说明在条件队列内发生过中断,此时await方法抛出中断异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 条件成立:说明在条件队列外发生的中断,此时设置当前线程的中断标记为为 true
// 中断处理 交给业务代码处理,如果你不处理 什么事也不会发生..
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
signal() 唤醒后的逻辑
public final void signal() {
// 判断当前调用signal方法的线程是否是持有锁的线程
if (!isHeldExclusively())
// 如果不是 则直接抛异常
throw new IllegalMonitorStateException();
// 获取条件队列的第一个node
Node first = firstWaiter;
// 不为null,则将第一个节点进行迁移到同步队列的逻辑
if (first != null)
doSignal(first);
}
doSignal()
private void doSignal(Node first) {
// 自旋
do {
/*
* 当前节点要进行出队,所以更新firstWaiter 指向下一个结点
* 如果当前等待队列只有一个节点的话,需要更新lastWaiter为null
*/
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 当前first节点出队,断开和下一个节点的关系
first.nextWaiter = null;
/*
* 条件1:transferForSignal(first) true->迁移成功 false->迁移失败,取反后 迁移失败会进入条件2的逻辑:
* 条件2:(first = firstWaiter) != null 当前first迁移失败,则将first更新为 first.next 继续尝试迁移..
* 直至迁移某个节点成功,或者 条件队列为null为止。
* 这里表示要么迁移一个节点成功 要么 队列为空跳出循环。
*/
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
AQS.transferForSignal()
/*
* 真正迁移节点的逻辑
* @return 返回true->表示迁移成功 返回false->表示迁移失败..
*/
final boolean transferForSignal(Node node) {
/*
* CAS修改的当前Node的状态为0,因为马上node要迁移到同步队列了,
* 成功:当前节点在条件队列中状态正常。
* 失败:
* 1.取消状态(线程await时 未持有锁,最终线程对应的node会设置为 取消状态)
* 2.node对应的线程 挂起期间,被其它线程使用 中断信号 唤醒过...(就会主队进入到 阻塞队列,这时也会修改状态为0)
* 失败返回false,尝试唤醒下一个节点
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* enq() 自旋进入同步队列,一定会入队成功,最终返回当前node进入同步队列后的前驱节点
* enq()方法参考AQS源码解析
* p 就是当前节点在同步队列的的前驱节点
*/
Node p = enq(node);
// 获取前驱节点的waitStatus
int ws = p.waitStatus;
/*
* 条件1:判断如果前驱节点的状态 > 0(取消),那么直接唤醒node。
* 条件2:前置条件:前驱节点的node状态 <= 0,那么尝试CAS修改为-1(signal),即前驱节点释放锁后唤醒后继节点。
* 设置失败说明前驱节点的状态突然被取消了,需要唤醒后继节点。
* 当前驱node对应的线程是lockInterrupt入队时的node,是会响应中断的,
* 外部线程给前驱node中断信号之后,前驱node会将状态修改为取消状态,并且执行出队逻辑。
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 走到这里 说明前驱节点状态处于取消状态,那么直接唤醒当前node对应的线程。
LockSupport.unpark(node.thread);
return true;
}
参考