Condition条件队列源码解析
一、Condition简介
Condition条件队列是一个单向链表,在使用时是依赖Lock对象的。他的Node和AQS中的node一样,只是没有前驱结点和后继结点,而是用nextWaiter来存储下一个结点的。Codition与Lock一起可以实现类似Object的监视器方法。它相比于Object提供的wait(),notify()方法更加灵活,颗粒度也更高。
二、Condition的await()源码解析
我们先来看下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();
}
以上的7个方法是Condition接口定义的,我们主要使用的也是这几个方法。接下来我们看下他的实现类ConditionObject 方法。我们可以在ReentrantLock的源码中的newCondition()方法,看到其实是创建了一个ConditionObject的对象。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//在Condition中使用firstWaiter指向了条件队列中的头结点
private transient Node firstWaiter;
//使用lastWaiter指向了条件队列中的尾结点
private transient Node lastWaiter;
/**
* Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
}
在ConditionObject中定义了firstWaiter和lastWaiter,分别指向了条件队列的头结点和尾结点。
接下来我们在看下主要方法await()的源码。
public final void await() throws InterruptedException {
//判断线程是否中断,如果被中断直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程封装进Node结点,状态设置为-2,将结点加入到条件队列尾部
Node node = addConditionWaiter();
//释放当前线程的锁,因为只有获取锁的线程才能进入await()方法中
int savedState = fullyRelease(node);
/*
* interruptMode指的是当前线程是否被中断的状态
* 0 表示没有被中断过
* -1 指在条件队列当中时被中断过
* 1 指的是在移除条件队列到阻塞队列时或已经在阻塞队列时出现了中断
*/
int interruptMode = 0;
/*
* isOnSyncQueue方法判断当前结点是否还在条件队列中true说明已经不在了
*/
while (!isOnSyncQueue(node)) {
//如果还在条件队列中,将线程挂起
LockSupport.park(this);
//判断当前结点是否正常唤醒
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/*
*acquireQueued将条件队列结点迁移到阻塞队列中后,挂起,直至被前驱结点唤醒获取锁
*acquireQueued()返回为true说明被中断唤醒
*interruptMode不为-1说明在条件队列中没被中断过
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
//将interruptMode 状态设置为1,表示在阻塞队列中被中断过
interruptMode = REINTERRUPT;
/*
* 判断node结点的nextWaiter为不为空,不为空将条件队列中状态不为-2的结点都清理掉
* 如果当前结点正好是队尾结点,那么不会去清理,在有新节点加入条件队列的时候会去清理
*/
if (node.nextWaiter != null)
//清理条件队列中的结点
unlinkCancelledWaiters();
//线程在条件队列或阻塞队列中被中断过
if (interruptMode != 0)
//在条件队列中被中断的抛出异常
reportInterruptAfterWait(interruptMode);
}
我们看到在调用await()方法之后
1.线程首先回去判断是否被中断
2.新建一个状态为-2的条件队列结点,将该线程封装进去,将该结点加入条件队列队尾
3.释放当前线程占有的锁
4. 循环判断结点是否还在条件队列中
5.挂起线程
6.线程被唤醒后判断是否是中断唤醒,如果是在条件队列中被中断唤醒,则将线程自旋加入阻塞队列的队尾,如果是线程在被signal()唤醒,在迁移到条件队列的途中或在则阻塞队列中被中断则设置interruptMode为-1或1
7.判断当前结点的下一个结点是否为空,清理一遍队列中状态不为-2的结点
8.判断线程是否被中断过,被中断过进行特殊处理
接下来我们一个一个看await()里面调用的方法
addConditionWaiter()方法
private Node addConditionWaiter() {
//获取条件队列尾结点
Node t = lastWaiter;
// 尾结点不为空且尾结点状态不为-2时,去清理一遍条件队列
if (t != null && t.waitStatus != Node.CONDITION) {
//清理条件队列中状态不为-2的结点
unlinkCancelledWaiters();
//获取新的尾结点
t = lastWaiter;
}
//新建状态为-2的node结点,将当前线程封装进去
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//尾结点为空时说明队列为空
if (t == null)
// 将新建的阶段作为头结点
firstWaiter = node;
else
//队列不为空时,将队尾的nextWaiter指向新建的node结点
t.nextWaiter = node;
//更新尾结点为当前结点
lastWaiter = node;
//返回当前结点
return node;
}
在 addConditionWaiter()方法中,主要是将当前线程封装进node结点,再将node结点入队的操作,还会去判断尾结点状态去对条件队列进行一次清理。
fullyRelease()方法
final int fullyRelease(Node node) {
//释放锁是否成功
boolean failed = true;
try {
//获取当前锁状态
int savedState = getState();
//通过release()方法释放锁资源,源码解析详解见AQS源码解析
if (release(savedState)) {
//释放锁成功
failed = false;
//返回锁释放的资源
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//如果释放锁资源失败执行
if (failed)
//将当前node状态设置为1
node.waitStatus = Node.CANCELLED;
}
}
在 fullyRelease()方法中,会去进行释放锁的操作,会记录下锁的状态,去完全释放锁。重入多次的锁也是完全释放,记录下重入的层数。如果锁释放失败会抛出异常并将结点状态设置为1.
isOnSyncQueue()方法
final boolean isOnSyncQueue(Node node) {
/*
* 1.当前结点状态为-2时,说明此时结点还在条件队列当中
* 2.当前结点不为-2时,可能某个线程通过signal()唤醒当结点,修改了此结点状态
* 3.判断前驱结点不为null,说明此时只是修改状态,还没向阻塞队列迁移
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/*
* 此时可能正在向阻塞队列入队,将前驱结点赋值后,进行cas将结点设置为尾结点的过程
* 必须CAS操作成功后才会设置前驱结点的后继结点为node,所以此时可能还没入队
* 但是当结点的后继结点不为空时,说明肯定入队了
*/
if (node.next != null)
return true;
/*
* 此时说明结点还在想阻塞队列迁移中,可能还没完成
*/
return findNodeFromTail(node);
}
isOnSyncQueue()方法主要是判断当前结点是否已经迁移到阻塞队列中去了,通过结点的状态,结点的前驱结点,后继结点进行判断
findNodeFromTail()
private boolean findNodeFromTail(Node node) {
//获取当前尾结点
Node t = tail;
//自旋,从队尾往前找,找到与当前结点相等的结点返回为true
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
findNodeFromTail()主要是在阻塞队列中从后往前找到当前结点。
checkInterruptWhileWaiting()判断是什么中断
private int checkInterruptWhileWaiting(Node node) {
//如果线程被中断,查找是在阻塞队列中断的还是条件队列中断
//没有中断返回0
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
主要是判断线程的是否被中断,在什么状态下被中断
transferAfterCancelledWait()
final boolean transferAfterCancelledWait(Node node) {
//线程被中断且还没被signal时CAS修改当前结点状态为0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//将当前结点加入到阻塞队列中
enq(node);
return true;
}
/*
* 如果当前结点已经被signal唤醒且状态被修改了
* 判断当前结点是否在阻塞队列中
*/
while (!isOnSyncQueue(node))
//状态被修改了但是还未进入阻塞队列时,短暂释放cpu
Thread.yield();
return false;
}
通过该方法,我们可以判断当前队列目前是处于在阻塞队列还是条件队列中,如果是处于条件队列中,则checkInterruptWhileWaiting()方法返回-1,处于阻塞队列返回1.
接下的acquireQueued()方法在AQS的源码中已经解析过了,主要是去获取资源,获取失败后改变前驱结点状态,将自己挂起。
unlinkCancelledWaiters()
private void unlinkCancelledWaiters() {
//获取条件队列头结点
Node t = firstWaiter;
Node trail = null;
//如果获取的t不为null循环
while (t != null) {
//获取t的下一个结点
Node next = t.nextWaiter;
//如果t结点的状态不为-2
if (t.waitStatus != Node.CONDITION) {
//将t出队
t.nextWaiter = null;
// trail为空说明,到目前没有正常的结点
if (trail == null)
//将t的下一个结点作为头
firstWaiter = next;
else
//如果正常的结点已经有了,则将t的后继结点作为正常结点的后继结点
trail.nextWaiter = next;
//如果后继结点为空,遍历完成了
if (next == null)
//将最后一个正常结点作为尾结点
lastWaiter = trail;
}
else
//状态为-1时说明是正常结点,将其复制给trail
trail = t;
//将t更新成下一个结点
t = next;
}
}
在 unlinkCancelledWaiters()方法中,主要是将整个条件队列遍历了一遍,将队列中状态不正常的结点全部出队,只留下正常的。
reportInterruptAfterWait()
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
//如果是在条件队列中被中断的,直接抛出异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
//在阻塞队列中中断的,再去中断当前线程,其余不做处理
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
当线程是被中断过时,会去处理是在条件队列中中断,还是阻塞队列中中断
三、await()方法流程图
四、Condition的signal()源码解析
我们在开看下signal()唤醒方法的源码
public final void signal() {
//判断当前线程是否是获取到了锁,必须获取到锁才能调用该方法
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取当前条件队列头结点
Node first = firstWaiter;
if (first != null)
//队列不为空时,调用此方法去唤醒头结点
doSignal(first);
}
首先调用signal()的线程必须是获取到了锁的,不然会直接抛异常,之后是通过doSignal()方法去唤醒条件队列中的第一个结点的。
doSignal()
private void doSignal(Node first) {
do {
//把当前头结点的下一个结点作为新的头结点并判断是否为null
if ( (firstWaiter = first.nextWaiter) == null)
//如果为null说明此时队列只有一个结点
lastWaiter = null;
//将头结点唤醒之后队列里面没有结点了,将尾结点也置于null
first.nextWaiter = null;
//将头结点迁移至阻塞队列中,成功返回true,失败返回false
} while (!transferForSignal(first) &&
//如果失败,将新的头结点复制给firse,且不为null的情况下,继续循环
(first = firstWaiter) != null);
}
该方法主要是去取头结点去进行唤醒
transferForSignal()
final boolean transferForSignal(Node node) {
/*
* CAS将结点的状态修改为0
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 将当前结点通过enq自旋入队
*/
Node p = enq(node);
//获取当前结点的前驱结点状态
int ws = p.waitStatus;
//如果前驱结点状态为1或将前驱结点状态改为-1失败,直接唤醒当前结点会去
//将结点前面失效的结点出队
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferForSignal()方法主要是
1.修改当前结点状态为0
2.将当前结点加入阻塞队列
3.判断前驱结点是否正常
4.前驱结点不正常直接唤醒当前结点对前驱结点进行出队
五、signal()方法流程图
六、总结
1.Condition队列主要数据结构是一个单向链表,依赖lock来实现线程的挂起和唤醒的
2.调用await()和signal()方法都需要获取到锁
3.await以后会完全释放锁,将线程放入一个单向链表的条件队列
4.被中断唤醒的线程会被加入到阻塞队列当中去
5.signal()会将条件队列的第一个结点迁移到阻塞队列的队尾当中去