Condition接口定义
Condition将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过await(),signal()来休眠/唤醒线程。
Object的监视器方法和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;
// 通知其他和当前锁对象绑定的任一Condition
void signal();
// 通知所有和当前锁对象绑定的任一Condition
void signalAll();
}
1、Condition提供了await()方法将当前线程阻塞,并提供signal()方法支持另外一个线程将已经阻塞的线程唤醒。
2、Condition需要结合Lock使用
3、线程调用await()方法前必须获取锁,调用await()方法时,将线程构造成节点加入等待队列,同时释放锁,并挂起当前线程
4、其他线程调用signal()方法前也必须获取锁,当执行signal()方法时将等待队列的节点移入到同步队列,当线程退出临界区释放锁的时候,唤醒同步队列的首个节点
ConditionObject
ConditionObject实现了Condition接口,定义在AbstractQueuedSynchronizer中,每个ConditionObject包含一个等待队列,队列节点在AQS中已Node定义,条件队列的实现原理可以类比AQS同步队列的同步队列实现。
等待队列的基本结构如下所示: image Condition拥有首尾节点的引用,而新增节点(调用Condition的await()方法)只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。
上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
同步队列和条件队列的整体关系如下:
1.每次调用Condition.await方法,相当于将同步队列的首节点(获得锁的节点)移动到Condition的等待队列的尾部。
2.有节点调用Condition.signal方法时,则会将等待队列里的首节点唤醒移动到同步队列的尾节点中。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
堵塞等待实现
在ConditionObject,实现了Condition的多个方法来进行条件堵塞等待,包括:
await()
void awaitUninterruptibly()
awaitNanos(long nanosTimeout)
await(long time, TimeUnit unit)
awaitUntil(Date deadline)
这里主要看await()方法实现,其他实现大同小异:
public final void await() throws InterruptedException {
// 检测被打断,直接抛出
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程封装成Node加入到等待队列尾部
Node node = addConditionWaiter();
//释放锁
int savedState = fullyRelease(node);
// 记录wait结束后的中断行为,有3个可能值:
// 0. 默认为0,表示不进行中断相关行为
// 1. REINTERRUPT=1:重新interrupt()函数进行中断
// 2. THROW_IE=-1:直接抛出中断异常
int interruptMode = 0;
//判断当前节点是否已经在同步队列中,如果是则退出循环,如果不是就阻塞当前线程
//其他线程如果发出了signal信号之后,会把等待队列的线程移入同步队列,此时就会退出循环,进入下面的重新获取锁的acquireQueued
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//其他发出signal信号的线程释放锁之后,该线程被唤醒并重新竞争锁
// acquireQueued是AQS的同步队列竞争锁逻辑实现,判断当前是否为节点,是则尝试获取锁,不是则堵塞直到成为首节点
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
// 取消node节点后续为取消状态的节点,这里等待状态不是CONDITION都被认为是取消状态
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter 插入等待队列
在实现开始尝试调用addConditionWaiter将当前线程封装成节点放进等待队列中,实现源码为:
private Node addConditionWaiter() {
// 获取尾部节点
Node t = lastWaiter;
// 如果尾节点被取消,开始等待队列清理取消状态节点
if (t != null && t.waitStatus != Node.CONDITION) {
// 从头开始遍历。去除状态异常节点
unlinkCancelledWaiters();
t = lastWaiter;//t指向最后一个状态正确的节点
}
// 生成一个新节点,状态为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)//列表为空,初始化为第一个节点
firstWaiter = node;
else // 成为原尾节点的下一个节点
t.nextWaiter = node;
// 更新尾节点
lastWaiter = node;
return node;
}
// 遍历清理取消状态节点实现
private void unlinkCancelledWaiters() {
// 先记为首节点,t代表遍历过程的每个节点
Node t = firstWaiter;
// trail为遍历到当前节点t前的的最后一个有效节点
Node trail = null;
while (t != null) {
// 缓存下一个节点,用于更新t
Node next = t.nextWaiter;
// 是否为取消状态
if (t.waitStatus != Node.CONDITION) {
// 消除引用关系
t.nextWaiter = null;
if (trail == null) // 所有节点无效
// 更新第一个有效节点为firstWaiter
firstWaiter = next;
else
// 记录有效节点的后置引用关联
trail.nextWaiter = next;
if (next == null) // 说明遍历结束,更新尾节点
lastWaiter = trail;
}
else // 更新最后一个有效节点
trail = t;
// 更新t,进入下一轮遍历
t = next;
}
}
fullyRelease释放持有的锁
之前说在调用await方法前,condition对象必须取得锁,如果未取得锁,会抛出异常,这是在fullyRelease中实现的,下面看源码:
final long fullyRelease(Node node) {
// 记录释放状态是否失败,默认为true
boolean failed = true;
try {
long savedState = getState();
// 尝试释放,释放成功返回true,否则返回false
if (release(savedState)) {
// 记录失败成功
failed = false;
return savedState;
} else {
// 释放锁失败,说明本来不持有锁,抛出异常
throw new IllegalMonitorStateException();
}
} finally {
// 失败则更新节点为取消状态,待从等待队列中移除
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
其中release是AQS的原生实现,这里不再分析。
isOnSyncQueue是否在同步队列中
在将当前线程插入等待队列和释放锁后,会调用isOnSyncQueue判断自己是否在同步队列中,如果在,说明被唤醒,否则继续进行堵塞,isOnSyncQueue实现如下
final boolean isOnSyncQueue(Node node) {
//如果当前线程为condition状态,说明在等待队列中正常等待
// 或者prev为null,prev是在同步队列中才有效的参数,说明当前不是同步队列的节点
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null)
// 如果该节点的next(不是nextWaiter,next指针在CLH队列中指向下一个节点)状态不为null,则该节点一定在CLH队列中
return true;
// 从同步队列尾节点遍历,看是否在同步队列
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;
}
}
中断响应处理
在判断isOnSyncQueue为false堵塞被唤醒后会检查线程是否因为中断被唤醒,这是因为LockSupport.park会响应线程中断,但LockSupport.park结束是否因为线程中断(还是唤醒),需要通过中断标识位判断,这在checkInterruptWhileWaiting中完成:
private int checkInterruptWhileWaiting(Node node) {
// 线程是否被中断
// 是则调用transferAfterCancelledWait判断中断处理行为
// 返回true表示需要抛出异常,false表示重新打断
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
//cas将该节点状态由CONDITION变成0
// 如果成功,调用enq将该节点从CONDITION队列添加到同步队列中(但是在CONDITION队列中的nextWaiter连接并没有取消)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 更新状态成功,可以安心入队
enq(node);
// 这里表示后续要抛出异常
return true;
}
// cas失败,则死循环判断直到进入同步队列,退出后走上面逻辑
while (!isOnSyncQueue(node))
Thread.yield();
// 这里表示后续要调用中断函数进行重新中断
return false;
}
堵塞唤醒
在Condition中,操作唤醒其他Condition对象的函数有signal()和signalAll(),两个方法的实现非常类似:
public final void signal() {
// 如果当前线程没有持有锁,抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取等待队列首节点
Node first = firstWaiter;
if (first != null)
// 唤醒首节点,移动到同步队列
doSignal(first);
}
public final void signalAll() {
// 如果当前线程没有持有锁,抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取等待队列首节点
Node first = firstWaiter;
if (first != null)
// 唤醒所有节点,移动到同步队列
doSignalAll(first);
}
从上面看到,两个函数的区别分别是doSignal和doSignalAll的调用,下面看看这两个函数的源码实现:
//对等待队列中从首部开始的第一个CONDITION状态的节点,执行transferForSignal操作,将node从等待队列中转换到同步队列队列中,同时修改同步队列中原先尾节点的状态
private void doSignal(Node first) {
do {
//当前循环将first节点从CONDITION队列transfer到同步队列
//从CONDITION队列中删除first节点,调用transferForSignal将该节点添加到同步队列中,成功则跳出循环
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 将CONDITION队列中所有node出队,逐个添加到同步队列末尾,同时修改它们在同步队列中前驱节点的状态,修改为signal成功,则不用在此处唤醒该节点的线程,唤醒工作交给同步队列中的前驱节点,否则需要在此处park当前线程。
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
//将first节点从CONDITION队列中出队
Node next = first.nextWaiter;
first.nextWaiter = null;
//将first节点在同步队列中入队,同时可能需要执行unpark操作
transferForSignal(first);
//更新first的指向
first = next;
} while (first != null);
}
在两个方法中,都调用了transferForSignal,内部主要实现将一个节点从等待队列移动到同步队列的逻辑,具体代码如下:
// 1. 先尝试将等待队列首节点由CONDITION转换为初始状态,如果失败,返回转义失败,
// 2. 调用enq()将该node添加到同步队列中,成为新的尾节点,返回原来的尾节点
// 3. 若同步队列原先尾节点为CANCELLED或者对原先尾节点CAS设置成SIGNAL失败,则唤醒node节点;若cas失败,说明节点在同步队列总前驱节点已经是signal状态了,唤醒工作交给前驱节点(节省了一次park和unpark操作)
final boolean transferForSignal(Node node) {
//如果CAS失败,则当前节点的状态为CANCELLED
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//enq将node添加到CLH队列队尾,返回node的prev节点p
Node p = enq(node);
int ws = p.waitStatus;
//如果p是一个取消了的节点,或者对p进行CAS设置失败,则唤醒node节点,让node所在线程进入到acquireQueue方法中,重新进行相关操作
//否则,由于该节点的前驱节点已经是signal状态了,不用在此处唤醒await中的线程,唤醒工作留给CLH队列中前驱节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}