前言
在上一篇中介绍了AQS中的同步队列:AQS:从原理到源码解读,条件队列相对于同步队列来说的话,内容并没有那么的多。因为在同步队列中是分了共享模式与独占模式,而在条件队列中,是没有共享模式的,条件队列中的节点都是独占式的,这一点会在接下来的源码中介绍到。
在开始条件队列的源码之前,我们以一段代码为例子:
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock=new ReentrantLock();
Condition condition = lock.newCondition();
Thread t1=new Thread(()->{
try {
lock.lock();
System.out.println("t1 get lock");
Thread.sleep(2000);
condition.await();
System.out.println("t1 continue");
Thread.sleep(2000);
System.out.println("t1 return");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
});
Thread t2=new Thread(()->{
try {
lock.lock();
System.out.println("t2 get lock and signal...");
Thread.sleep(2000);
condition.signal();
System.out.println("t2 return");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
});
t1.start();
Thread.sleep(1000);
t2.start();
t1.join();
t2.join();
}
可以看到,我们使用条件队列的话,一般会涉及到三个步骤:
- 获取锁:也就是lock操作
- await:会让当前线程进入等待状态
- signal:唤醒条件队列中的第一个线程。
lock方法则是获取锁,这个没什么好说的,在上一篇已经介绍过了,所以条件队列中主要就是await方法与signal方法,接下来就从源码角度来看这两个方法,还是老规矩,对于部分代码,会在上面进行标记。
Await
首先我们需要知道的是,一个同步队列是可以有多个条件队列的,而同步队列则是将首节点放到条件队列的尾节点。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//初始化链表的功能,设置当前线程为链尾
Node node = addConditionWaiter();
//释放当前节点持有的所有资源
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断node是否在同步队列,注意是同步队列而不是条件队列
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//走到这里说明已经成功获取到了独占锁
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//处理被中断的情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法中也是有很多子方法,我们首先看第一个addConditionWaiter:
private Node addConditionWaiter() {
//创建一个condition类型的节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
//如果队尾的元素不是CONDITION状态,就将其清除掉
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
//可能t之前引用的节点被删除了,所以要重新引用。
t = lastWaiter;
}
//将当前线程设置为CONDITION类型的节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)//说明队列中没有节点
firstWaiter = node;
else
t.nextWaiter = node;//否则就将节点放在队列的队尾
lastWaiter = node;
return node;
}
//遍历队列,将那些非Condition的节点删除掉。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
//记录在循环中上一个waitStatus有效的节点
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;//在对每个节点状态判断前,记录他的下一个节点,
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;//如果节点状态不为CONDITION,那么就将他的下一个节点置为null,
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;//否则就直接加到队尾。
if (next == null)
lastWaiter = trail;
}
else //记录有效接待你并向后移
trail = t;
t = next;
}
}
这一步就是将节点放入队列中,这个方法其实与独占、共享式的中将线程放入队列中的方法是一样的,只不过这个是节点的状态是condition状态的。
在开头的例子中我们看到,使用await、signal的时候,会先获取到锁,所以我们在将节点放入条件队列之前,节点就已经获取了锁。因此我们需要将获取到的锁给释放掉。
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取当前state并释放,这从另一个角度说明必须是独占锁。
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//如果释放资源失败,那么就把节点取消,由这里就能看出来上面添加节点的逻辑中只需要判断最后一个节点是否被取消就可以了。
if (failed)
node.waitStatus = Node.CANCELLED;//如果release失败则将节点的waitStatus这是为CANCELLED
}
}
释放锁的过程也很简单,也就是获取当前状态,释放锁。至于release操作,在第一篇中已经介绍过了,不熟悉的话可以翻到上一篇看下。
//判断node是否在同步队列,注意是同步队列而不是条件队列。调用signal方法会将节点从条件队列移动到同步队列
while (!isOnSyncQueue(node)) {
LockSupport.park(this);//如果不在同步队列则阻塞当前线程
//这里被唤醒,可能是signal操作,也可能是中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;//检查线程被唤醒是否是因为被中断,如果是则跳出循环,否则会进行下一次循环,因为被唤醒前提是进入同步队列,所以下一次循环也必然会跳出循环
}
因为节点会从同步队列放到条件队列的尾端,经过了前两步操作后,现在就是要确定节点是否还在同步队列。
final boolean isOnSyncQueue(Node node) {
//同步队列是有前缀节点的,而且条件队列中节点的状态是condition,如果这两个有一个满足,那么就说明是在条件队列,而不是在同步队列。
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;// 如果waitStatus为CONDITION 或 node没有前驱节点,则必然不在同步队列,直接返回false
//只有同步队列才会使用next,条件队列使用的都是nextWaiter字段。
if (node.next != null) // If has successor, it must be on queue
return true;//如果有后继节点,那么一定在同步队列中,返回true
return findNodeFromTail(node);
}
// 从同步队列的尾节点开始向前遍历,找到返回true,否则为false
//因为条件队列中的节点是被加到同步队列的尾端的,所以可以从同步队列的尾部开始进行查找。
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
这里主要是利用同步队列中节点的属性与条件队列中节点的属性:比如只有条件队列中节点的状态才是condition,条件队列中是nextWaiter字段,而同步队列是next等。先通过这些直接的属性进行判断节点是否在同步队列,如果这些条件还无法判断的话,才会进行复杂的判断,通过对同步队列从后向前遍历,最终得到节点是否存在于同步队列。
如果节点不在同步队列了,那么就阻塞当前线程,因为线程被唤醒可能会是因为signal操作,也可能是被中断,类型不同,返回值也会不同:
//判断在阻塞过程中是否被中断,THROW_IE表示在调用signal之前被中断,REINTERRUPT表示在调用signal之后中断,0表示没有被中断
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//线程是否因为中断从park中唤醒
//修改节点状态并加入同步队列。
//返回true表示节点由中断加入同步队列,返回false表示由signal加入同步队列
final boolean transferAfterCancelledWait(Node node) {
//如果CAS成功,暂且认为中断发生后,signal被调用
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//这个其实就不多说了,在第一篇中也涉及到了。
enq(node);
//true表示中断先于signal发生
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
//如果上面设置失败,说明节点已经被signal唤醒,由于signal操
// 作会将节点加入同步队列,我们只需自旋等待即可
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
上面的方法执行完毕后,说明节点走到已经被加入到同步队列或者中断中,然后就该尝试获取锁了。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
final boolean acquireQueued(final Node node, int arg) {
//返回true说明未获取到资源,需要进行中断,如果返回false,那么说明获取到了资源
boolean failed = true;
try {
boolean interrupted = false;//是否中断
for (;;) {
//当前节点的pre节点。
final Node p = node.predecessor();
//需要注意的是,头节点是个虚节点,头节点后面的节点才是真正存数据的节点。
if (p == head && tryAcquire(arg)) {//如果前驱节点是头节点,并且当前线程获取锁成功。
setHead(node);//那么就将当前节点设置为头节点,node的thread设置为null,
//将头节点从队列中删除,因为此时当前节点已经获取到了锁,没有必要存放到队列中了。
p.next = null; // help GC
failed = false;
return interrupted;
}
//说明前驱节点不为头节点并且没有获取到同步锁
//判断当前线程是否需要阻塞
//parkAndCheckInterrupt阻塞当前线程,并且检验线程的状态。
//因为前驱节点不是头节点,而且也没有获取到同步锁,所以要将他阻塞掉,减少资源的请求
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//以上执行完之后,会执行selfInterrupt
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
独占锁获取完毕后,就需要将那些非condition的节点删除掉。
//走到这里说明已经成功获取到了独占锁
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//遍历队列,将那些非Condition的节点删除掉。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
//记录在循环中上一个waitStatus有效的节点
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;//在对每个节点状态判断前,记录他的下一个节点,
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;//如果节点状态不为CONDITION,那么就将他的下一个节点置为null,
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;//否则就直接加到队尾。
if (next == null)
lastWaiter = trail;
}
else //记录有效节点并向后移
trail = t;
t = next;
}
}
至此await中的主体流程已经执行的差不多了,我们来看下最后的部分:
//处理被中断的情况
if (interruptMode != 0)
//如果是THROW_IE,说明signal之前发生中断
//如果是REINTERRUPT,signal之后中断,
//所以成功获取资源后会调用selfInterrupt()
reportInterruptAfterWait(interruptMode);//如果跳出while循环是因为被中断,则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程
根据中断时机选择抛出异常或者设置线程中断状态
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
Signal
signal方法相较于await方法,则是要简单许多了。调用signal()方法,将会唤醒在等待队列中等待时间最长的节点(也就是首节点),在唤醒节点之前,会将节点移到同步队列中。至于Signal方法,主体已经用注释标记起来了。
public final void signal() {
//判断当前线程是否为资源持有者,也说明了为啥条件队列是独占锁,因为共享锁是不会记录持有锁的线程的。
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//唤醒队列的第一个节点
Node first = firstWaiter;
if (first != null)
//唤醒条件队列的头结点
doSignal(first);
}
//1、可以看到doSignal()的主体是一个do-while循环,循环体内将首节点(再次提醒该节点是条件队列的首节点)的nextWaiter赋值给firstWaiter,并将首节点的nextWaiter赋值为null(相当于移除首节点),另外如果first节点的后继节点为空,则将lastWaiter赋值为null。
// 2、判断条件是transferForSignal()的返回值为false并且first节点的后继节点(firstWaiter)不为null。transferForSignal()会线程安全(如果cas失败返回false)的将first节点从条件队列移动到同步队列,(first = firstWaiter) != null表达式在不成立的情况下,配合while会向下遍历条件队列,直到节点成功移动到同步队列并且first节点的后继节点为null。
private void doSignal(Node first) {
do {
//后续的等待条件为空,说明condition队列中只有一个节点。
//其实就是向后面去找一个不为空的节点。
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
//transferForSignal是真正唤醒头节点的地方
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//当前节点等待状态改变失败,则说明当前节点不是CONDITION状态,那就不能进行接下来的操作,返回false
//0是正常状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//放入等待队列队尾中,并返回之前队列的前一个节点
Node p = enq(node);//p是队列之前的最后一个节点,
int ws = p.waitStatus;
//如果节点没有被取消,或更改状态失败,则唤醒被阻塞的线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
结尾
条件队列相较于独占锁、共享锁,可能关注点没那么多。实际上在我们了解了独占式与共享式之后,再了解条件队列并不是一件难事,毕竟条件队列本身也就是在同步队列的基础之上才有的。