这时AQS的第三篇文章,在第二篇文章中,我们分析了同步队列的入队与出队,在第三盘文章中,我们去分析条件队列的创建,入队,出队。
ConditionObject 介绍
条件对象,在MESA模型中,条件对象可以有多个,每个条件对象都有一个对应的条件队列;当调用wait方法时,添加到条件队列;调用notify
方法时,从条件队列转移到同步队列;
在java中,ConditionObject 对象实现了Condition接口,提供await(),signal(),signalAll()等一系列等待唤醒方法;
ConditionObject 含有队首,队尾两个属性
/** First node of condition queue. 队首 */
private transient Node firstWaiter;
/** Last node of condition queue. 队尾*/
private transient Node lastWaiter;
且两个属性都没有关键字 volatile ,是因为条件队列只有在获取独占锁的情况下才可以入队;也意味着调用 await,signal等方法,需要在持有锁的情况下才可以调用;
条件队列的创建,入队
在同步队列创建时,有addWaiter方法,在条件队列创建中有 addConditionWaiter();
/**
* Adds a new waiter to wait queue.
* 添加一个新的条件等待节点到条件等待队列
* @return its new wait node
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果尾节点被取消了,清理出所有的取消节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 调用Node的构造器,创建了一个节点为-2的等待队列节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
// 尾节点为空,则将当前节点作为首节点
firstWaiter = node;
else
t.nextWaiter = node;
// 当前节点赋值给为尾节点,尾插法
lastWaiter = node;
return node;
}
总结看来,就是使用尾插法插入,使用nextWaiter构造一个单向链表;当队列尚未构造时,既尾节点为空时,则当前节点为头节点;
出队
再插入时,如果检测到尾节不是等待状态,则需要清理出队;同时,当条件队列转移到同步队列时,也会涉及到出队;先从见到过的 unlinkCancelledWaiters 分析;
清理取消的节点
/**
* Unlinks cancelled waiter nodes from condition queue.
* 从条件队列中清除取消的节点
* Called only while holding lock. This is called when
* 仅当获取锁后才能撤销。
* cancellation occurred during condition wait, and upon
* 这个调用当取消发生在条件等待中,且当插入一个新节点时发现
* insertion of a new waiter when lastWaiter is seen to have
* 尾节点已经被取消了。
* been cancelled. This method is needed to avoid garbage
* 需要这个方法在没有通知信号时避免垃圾保留
* retention in the absence of signals. So even though it may
* require a full traversal, it comes into play only when
* 因此尽管该方法需要一个完整的遍历,但它只发生在没有信号的情况下
* timeouts or cancellations occur in the absence of
* 超时或者取消时。
* signals. It traverses all nodes rather than stopping at a
* 它遍历全部节点去清理所有指向垃圾节点的引用
* particular target to unlink all pointers to garbage nodes
* without requiring many re-traversals during cancellation
* 替代了多次的重复遍历,当取消风暴间
* storms.
*/
private void unlinkCancelledWaiters() {
// 获得头节点为当前节点
Node t = firstWaiter;
// 轨迹为null
Node trail = null;
// 当前节点不为null
while (t != null) {
// 获取当前节点的下一个节点
Node next = t.nextWaiter;
// 如果当前节点被取消
if (t.waitStatus != Node.CONDITION) {
// 清空当前节点的nextWaiter引用
t.nextWaiter = null;
// 如果上一次处理的节点为null,将下一个节点赋值为头节点
// 否则将下一个节点作为上一次节点的下一个节点
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
// 如果下一个节点为空,则尾节点为上一个节点
if (next == null)
lastWaiter = trail;
}
// 如果当前节点没有被取消,则上一次遍历节点赋值为当前节点,开始下一次循环
else
trail = t;
// 将下一个节点赋值为将要遍历的节点,继续循环
t = next;
}
}
就是从头节点开始遍历,剔除取消的节点,维护着单项链表的链接,及头节点和尾节点的引用;
节点转移到同步队列前的出队
/**
* Removes and transfers all nodes.
* 移除并转移所有的等待节点
* @param first (non-null) the first node on condition queue
*/
private void doSignalAll(Node first) {
// 置空所有的头节点,尾节点
lastWaiter = firstWaiter = null;
do {
// 取出当前节点(头节点)的下一个节点
Node next = first.nextWaiter;
// 将当前节点nextWaiter引用置为空
first.nextWaiter = null;
// 使当前节点入同步队列
transferForSignal(first);
// 下一个节点赋值给当前节点,进行下一次循环
first = next;
} while (first != null);
}
/**
* Removes and transfers nodes until hit non-cancelled one or
* 移除并转移节点直到命中一个非取消的等待节点
* null. Split out from signal in part to encourage compilers
* 从signal方法中分离出来作为一个单独方法,使为了鼓励编译器优化
* to inline the case of no waiters.
* (编译器优化部分,不做深入了解)
* @param first (non-null) the first node on condition queue
*/
private void doSignal(Node first) {
do {
// 如果当前节点的下一个节点为空,则将尾节点置为null,表明自己就是尾节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 清理当前节点的nextWaiter引用
first.nextWaiter = null;
// 转移失败且下一个节点不为空继续循环
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
在出队时,都有一种节点为取消的节点,下一节我们分析取消发生的时机;
等待节点的取消转移
类比同步队列取消 cancelAcquire 的方法,一般是在超时或者调用tryAcquire方法时失败时发生;我们研究下等待节点的取消;取消发生的时机,是在于await()超时时;
当调用不具有时间限制的await方法时,只会加入条件队列的尾部,等待signal信号转移到同步队列;而具有时间限制的await方法时,加入条件队列后,当超时后会尝试取消等待,尝试将状态切换到0;一旦无法完成cas状态切换,则当前线程会一直让出时间片,直到它已经被同步到同步队列上;代码分析详见 第二篇transferAfterCancelledWait方法解析;
与singal的区别在于,signal是先出条件队列,然后再入同步队列;而取消是不出条件队列,仅是将状态改为0,再入同步队列;
总结
目前,我们已经分析了条件队列出队,入队,取消的场景,与同步队列的交互也已经考察;下一篇我们将分析AQS的使用;