Condition源码讲解
简介
Condition是一个等待队列,作用类似与我们Object类中的wait()以及notify()方法,作用都是用于线程的唤醒与等待
Condition比我们在Object中的等待队列作用要更强大,更灵活,像我们的ConditionObject可以利用newCondition()来产生多个等待队列
下面来介绍Condition的源码与实现
首先是接口Condition,它定义了我们的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();
}
然后重点介绍AQS中的Condition接口实现----ConditionObject
先从属性开始进行讲解
重要属性
//头节点
private transient AbstractQueuedSynchronizer.Node firstWaiter;
//尾节点
private transient AbstractQueuedSynchronizer.Node lastWaiter;
注意看,这里的节点的类型是我们的AQS中的Node类型,所以可以知道我们等待队列和同步队列中的节点是一样的,然后先来看看他的模型是怎样的
可以看到这个就是一个典型的双端队列,这里前继没有给出,其实是可以有指向前面的指针的,但是等待的线程就是等待者,只负责等待,唤醒的线程就是唤醒者,只负责唤醒,因此每次要执行唤醒操作的时候,直接唤醒等待队列的首节点就行了。等待队列的实现中不需要遍历队列,因此也不需要prev指针,所以在图上没有显示前继指针
每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。
在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。.
然后来看看重要的方法
重要方法
await()方法
public final void await() throws InterruptedException {
//如果当前线程中断
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入到等待队列
AbstractQueuedSynchronizer.Node node = addConditionWaiter();
//释放锁,等待会释放自己的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
/* **
* 检测此节点的线程是否在同步队列上,即还没有被signal,则将当前线程阻塞
* 如果不在,说明这个线程不具备竞争锁的资格
* 知道检测到此节点到同步队列上
*/
while (!isOnSyncQueue(node)) {
//先挂起线程,这里会等待唤醒
LockSupport.park(this);
//如果已经中断了,直接退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 退出了上面自旋说明当前节点已经在同步队列上,但是当前节点不一定在同步队列队首。
// acquireQueued将阻塞直到当前节点成为队首,
// 即当前线程获得了锁。然后await()方法就可以退出了,让线程继续执行await()后的代码。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理条件队列中的不是在等待的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter()方法
private AbstractQueuedSynchronizer.Node addConditionWaiter() {
//获取到等待队列中的尾节点
AbstractQueuedSynchronizer.Node t = lastWaiter;
//如果Node的节点状态不是condition,说明这个节点不在等待状态
if (t != null && t.waitStatus != AbstractQueuedSynchronizer.Node.CONDITION) {
//需要清除掉等待队列中不是condition状态的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//当前线程新建节点,状态是condition
AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(Thread.currentThread(), AbstractQueuedSynchronizer.Node.CONDITION);
//判断头是不是空
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
final int fullyRelease(AbstractQueuedSynchronizer.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 = AbstractQueuedSynchronizer.Node.CANCELLED;
}
}
final boolean isOnSyncQueue(AbstractQueuedSynchronizer.Node node) {
//如果节点状态是condition或者前驱节点是null,返回false
if (node.waitStatus == AbstractQueuedSynchronizer.Node.CONDITION || node.prev == null)
return false;
//如果后继系欸但部位null,那么肯定在同步队列中
if (node.next != null)
return true;
return findNodeFromTail(node);
}
/* **
* 删除掉条件队列中不为condition的节点
* @param
* @return void
* @author zengyiwen
* @date 16:31 2022/1/18
*/
private void unlinkCancelledWaiters() {
AbstractQueuedSynchronizer.Node t = firstWaiter;
AbstractQueuedSynchronizer.Node trail = null;
//遍历节点
while (t != null) {
AbstractQueuedSynchronizer.Node next = t.nextWaiter;
if (t.waitStatus != AbstractQueuedSynchronizer.Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
总的来说,流程就是这样的
1、 获取了锁的线程调用我们的await()方法,
2、 将自己的线程封装成Node节点然后加入到等待队列
3、 释放自己的资源,
4、 自旋判断自己的是不是被signal()了,也就是是不是出现在我们的同步队列了,没有出现的话代表没人唤醒,继续等待
5、 如果被唤醒了,因为自己的资源已经被释放了,所以自己需要继续抢占资源,所以又会加入到同步队列
6、 清除掉等待队列中不处于等待状态的节点
然后来看看我们的唤醒流程
/* **
* 通知方法
* 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
* 如果线程已经获取了锁,则将唤醒条件队列的首节点
* 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中
* 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。
* @param []
* @return void
* @author zengyiwen
* @date 16:32 2022/1/18
*/
public final void signal() {
//检测当前线程是不是拥有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//唤醒条件队列中的第一个节点
AbstractQueuedSynchronizer.Node first = firstWaiter;
if (first != null)
doSignal(first);
}
/* **
* 1.修改头节点
* 2.调用transferForSignal将节点移到CLH同步队列
* @param
* @return void
* @author zengyiwen
* @date 16:34 2022/1/18
*/
private void doSignal(AbstractQueuedSynchronizer.Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
负责将节点加入到同步队列并且唤醒线程
final boolean transferForSignal(AbstractQueuedSynchronizer.Node node) {
//将节点变为初始状态0,CAS修改
if (!compareAndSetWaitStatus(node, AbstractQueuedSynchronizer.Node.CONDITION, 0))
return false;
//加入到SYN队列中,
AbstractQueuedSynchronizer.Node p = enq(node);
//获取到节点的状态
int ws = p.waitStatus;
//如果节点的状态是cancel,或者CAS失败,就会直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, AbstractQueuedSynchronizer.Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
还有singnalAll()
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
AbstractQueuedSynchronizer.Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(AbstractQueuedSynchronizer.Node first) {
lastWaiter = firstWaiter = null;
do {
AbstractQueuedSynchronizer.Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
唤醒就很简单了,就是尝试修改等待队列的头节点,同时将节点移到同步队列中去获取资源,并且调用unpark方法,让他继续去执行await方法中的后半部分,因为在await中被阻塞了,所以后面的流程就是调用aquiredQueue()方法来进行自旋获取锁
Condition的原理
Condition的本质就是等待队列和同步队列的交互:
当一个持有锁的线程调用Condition.await()时,它会执行以下步骤:
1、 构造一个新的等待队列节点加入到等待队列队尾
2、 释放锁,也就是将它的同步队列节点从同步队列队首移除
3、 自旋,直到它在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断
4、 阻塞当前节点,直到它获取到了锁,也就是它在同步队列上的节点排队排到了队首。
当一个持有锁的线程调用Condition.signal()时,它会执行以下操作:
1、 从等待队列的队首开始,尝试对队首节点执行唤醒操作;如果节点CANCELLED,就尝试唤醒下一个节点;如果再CANCELLED则继续迭代。
2、 对每个节点执行唤醒操作时,首先将节点加入同步队列,此时await()操作的步骤3的解锁条件就已经开启了。然后分两种情况讨论:
3、 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒当前节点对应的线程,此时await()方法就会完成步骤3,进入步骤4.
4、 如果成功把先驱节点的状态设置为了SIGNAL,那么就不立即唤醒了。等到先驱节点成为同步队列首节点并释放了同步状态后,会自动唤醒当前节点对应线程的,这时候await()的步骤3才执行完成,而且有很大概率快速完成步骤4.
总结
为什么同步队列是双向的,而等待队列是单向的呢?
之所以同步队列要设计成双向的,是因为在同步队列中,节点唤醒是接力式的,由每一个节点唤醒它的下一个节点,如果是由next指针获取下一个节点,是有可能获取失败的,因为虚拟队列每添加一个节点,是先用CAS把tail设置为新节点,然后才修改原tail的next指针到新节点的。因此用next向后遍历是不安全的,但是如果在设置新节点为tail前,为新节点设置prev,则可以保证从tail往前遍历是安全的。因此要安全的获取一个节点Node的下一个节点,先要看next是不是null,如果是null,还要从tail往前遍历看看能不能遍历到Node。
而等待队列就简单多了,等待的线程就是等待者,只负责等待,唤醒的线程就是唤醒者,只负责唤醒,因此每次要执行唤醒操作的时候,直接唤醒等待队列的首节点就行了。等待队列的实现中不需要遍历队列,因此也不需要prev指针。