通过前面的文章(JUC--Condition学习(一)简介和使用),我们对Condition有了一个初步的认识,并且我们也知道了如何使用Condition,现在我们就来看一看Condition到底是如何实现线程的等待和唤醒的。
在我们学习AQS的时候,我们知道当线程获取锁失败的时候会进入CLH队列,以等待状态(WAITING)存在于同步队列。而当我们调用Condition.await()方法后线程会以等待状态进入等待队列。
其实不管是等待队列还是同步队列都是依靠AQS的内部类Node来建立队列的。
那么等待队列和同步队列又有什么区别呢?我们直接看两张图就明白了。
(1)同步队列图
(2)等待队列图
上面第一张图是同步队列第二张是等待队列,我们可以看出等待队列每个节点仅仅持有指向下一节点的引用,即等待队列是单向链表,而同步队列是双向链表。
调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关联的锁。
通过查看源码,我们知道接口Condition的实现都交给了AQS的内部类ConditionObject。所以await函数的实现其实也在ConditionObject里面。
首先我们来看一下该函数的调用顺序。
首先我们来看一下await方法:
public final void await() throws InterruptedException {
//如果线程中断则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程添加进入条件队列
Node node = addConditionWaiter();
//释放线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//直到检测到此节点在同步队列上,否则一直循环(等待就体现在这个地方)
while (!isOnSyncQueue(node)) {
//挂起当前节点
LockSupport.park(this);
//在等待过程中获取中断状态,如果不为0,则已经线程中断,直接break
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//竞争同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理条件队列中不是在等待状态的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//针对中断状态不为0的情况,响应中断(要么抛出异常,要么自我中断)
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
方法的基本逻辑在注释中已经有体现了,我们接下来看看加入条件队列是如何实现的,其实这里我们可以猜想加入条件队列(即等待队列)无非就是生成节点,将节点添加到条件队列的尾节点后面,设置等待状态。那么猜想对不对呢?如下:
//将当前线程加入条件队列
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果尾节点被删除则清除尾节点.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//生成节点,并将节点加入条件队列当中
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
从上面可以看出我们的猜想是正确的,无非就是多了一个清除已经删除的节点的操作。那么到底是如何清除已经删除的节点的呢?
//将已经删除的等待节点从条件队列中移除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
//首节点不为空
while (t != null) {
Node next = t.nextWaiter;
//将节点的等待状态不为CONDITION的从条件队列中移除
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
上面我们看了如何将节点加入条件队列,下面我们来看看如何释放节点持有的锁。
//释放节点持有的锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取节点持有锁的数量
int savedState = getState();
//释放节点持有的锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
从上面的代码我们可以猜想释放节点的锁最终就是改变同步状态,改变持有锁的线程,唤醒后继节点。
//释放节点持有的锁
public final boolean release(int arg) {
//释放节点持有的锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒首节点的后继节点
unparkSuccessor(h);
return true;
}
return false;
}
await方法判断节点不在同步队列上的时候将阻塞线程,我们来看一看如何判断节点是否在同步队列上:
//判断节点是否在同步队列上
final boolean isOnSyncQueue(Node node) {
//由于当前节点的状态是CONDITION,所有返回false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
isOnSyncQueue方法就比较简单了,这里当前的节点是CONDITION状态,所以直接返回false。
在while循环中,我们可以看见当当前节点不在同步队列中就直接挂起当前线程,进行等待。循环直到当前节点在同步队列中的时候才继续后面的执行,针对后面处理中断的其余代码,这里就不继续分析了,比较简单,可以直接查看源码。
调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。
我们依然来看一下signal函数的调用顺序:
我们依次来查看上面方法的源码:
public final void signal() {
//判断当前线程是否拥有独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//首节点不为空则唤醒首节点
if (first != null)
doSignal(first);
}
从上面我们可以看出signal方法的实现主要是依靠doSignal唤醒首节点来实现的。
private void doSignal(Node first) {
do {
//修改头节点,完成旧头节点的移除工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
//循环直到将首节点从条件队列移入同步队列成功或者首节点为空
} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
接下来我们看一看到底是怎样将首节点进行转移的。
final boolean transferForSignal(Node node) {
//删除等待状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//添加进入等待同步队列,返回同步队列中的前继节点
Node p = enq(node);
int ws = p.waitStatus;
//如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
上面就是针对Condition核心方法的分析,其余的方法原理基本一样,这里就不逐一分析了。
谢谢关注,如有不同看法欢迎指正。