Condition的使用和实现
我们知道,任意一个Java Object,都拥有一组监视器方法,主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法和synchronized同步关键字配合,可以 实现等待/通知模式。
Condition接口也提供了类似Object的监视器方法,和Lock配合也可以实现等待/通知模式,但是这两者在使用以及功能上还是有比较大差别的。
Condition定义了等待/通知这两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。而Condition对象是由Lock对象,也就是调用Lock对象的newCondition()方法创建出来的,我们前面讲Lock接口的时候也提到过,换句话说,Condition其实是依赖Lock对象的。
使用
Condition的使用方式比较简单,只是需要注意下在调用方法前先要获取到锁。
下面我们就通过一个有界队列的示例来深入了解下Condition的使用方式。
首先,介绍下有界队列,顾名思义,有界队列就是一种特殊的队列,当队列为空时,队列的获取操作就会阻塞获取线程,直到队列中有新增元素,当队列满了,队列的插入操作就会阻塞插入线程,直到队列出现“空位”。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue<T> {
private Object[] items;
// 添加的下标,删除的下标和数据当前的数量
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
// 添加一个元素,如果数组满了,就让线程进入到等待队列,直到数组有空位
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length) // 防止虚假唤醒
notFull.await();
items[addIndex] = t;
if (++addIndex == items.length)
addIndex = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 从头删除一个元素,如果数组是空的,则删除线程进入等待状态,直到有新添加元素
@SuppressWarnings("unchecked")
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) // 防止虚假唤醒
notEmpty.await();
Object x = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
add(T t)
BoundedQueue通过add(T t)方法添加一个元素,通过remove()方法移出一个 元素。
首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时, 表示数组已满,就会调用notFull.await(),当前线程随之释放锁并进入等待状态。
如果数组数量不 等于数组长度,表示数组没满,就添加元素到数组中,同时通知等待在notEmpty上的线程,告知数 组中已经有新元素可以获取。
注意,在添加和删除方法中使用while循环而不是if判断,目的就是为了防止过早或意外的通知,只有条件符合才能够退出循环。
回想下之前提到的等待/通知的经典范式,两者是非常类似的。
实现
Condition接口的实现 ConditionObject其实是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要 获取相关联的锁。每个Condition对象都包含着一个队列(下面我们就把它叫做等待队列),这个队列是Condition对象实现等待/通知功能的关键所在。
所以呢,要分析Condition的实现,关键内容就是这3个:
等待队列、等待和通知,接下来我们提到的Condition如果没有特别说明都指的是ConditionObject类。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,这个线程就是 在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么这个线程就会 释放锁、构造成节点加入等待队列并进入等待状态。
事实上,这个节点的定义其实复用了同步器中节点 的定义,也就是 AbstractQueuedSynchronizer中的Node类。
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)这两个属性。当前线程调用Condition.await()方法之后,就会用当前线程来构造节点,并把节点从尾部 加入到等待队列里面,先来看下等待队列的基本结构。
可以看到,Condition拥有首尾节点的引用,而新增节点只需要把原有的尾节点的nextWaiter 属性指向它,并且更新尾节点就可以了。
注意,上面的节点引用更新的过程并没有使用CAS来保证线程安全,原因就在于调用 await()方法的线程肯定是获取了锁的线程。
对比下 Object的监视器模型,一个对象只有一个同步队列和等待队列,而并发包里的 Lock(更确切地说是同步器)有一个同步队列和多个等待队列,他们是这样对应的。
Condition的实现其实是同步器的内部类,所以每个Condition实例都能够访问到同步器 提供的方法。
大致了解了等待队列的结构之后,我们来看看等待的实现方式。
await()
调用Condition的await()方法(或者以await开头的方法),会让当前线程进入等待队列并且释放锁,同时线程状态会变为等待状态。而当线程从await()方法返回时,当前线程一定已经获取了和Condition相关联的锁。
如果从队列,也就是同步队列和等待队列的角度看await()方法,当调用await()方法时,相当于同步队列的首节点,也就是获取了锁的节点 移动到了Condition的等待队列里面。
注意,同步队列的首节点并不会直接加入等待队列,而是通过 addConditionWaiter()方法把当前线程构造成一个新的节点 然后再加入到等待队列中
来看下代码,Condition的await()方法
public final void await() throws InterruptedException {
if (Thread.interrupted())
//若线程已被中断(例如 Thread.interrupted() 返回 true),直接抛出 InterruptedException
throw new InterruptedException();
Node node = addConditionWaiter(); //将当前线程封装为节点,添加到条件变量对应的等待队列尾部
//完全释放当前线程持有的锁(注意是彻底释放,包括重入锁的全部计数)。
//例如,线程持有锁时多次重入,此方法会将锁状态重置为初始值
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) { //检查是否被唤醒并移入同步队列
LockSupport.park(this);// 挂起线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
//检查中断标记 checkInterruptWhileWaiting(node),若中断发生则退出循环
break;
}
//退出等待循环后,通过 acquireQueued(node, savedState) 重新在同步队列中排队获取锁(与普通锁竞争逻辑一致)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
//处理线程重新获取锁过程中可能发生的中断,并调整中断模式
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
//清理条件队列(Condition Queue)中已被取消的等待节点,保持队列的干净和高效
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); //上报中断状态
}
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException(); // 等待过程的中断直接抛出异常
else if (interruptMode == REINTERRUPT)
selfInterrupt(); // 重新获取锁过程的中断,设置中断标记位,由上层处理
}
调用这个方法的线程必须是成功获取了锁的线程,也就是同步队列中的首节点,这个方法会把当前线程构造成节点并加入等待队列里面,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程就会进入等待状态。
而当等待队列中的节点被唤醒,被唤醒节点的线程就会开始尝试获取同步状态。
中断处理:
假设线程A调用
await()
后被唤醒,进入同步队列排队等待锁:
若此时线程再次被中断(如
interrupt()
调用),则acquireQueued()
返回true
;若原
interruptMode
是0
(无中断或已处理),则更新为REINTERRUPT
,后续在reportInterruptAfterWait()
中调用selfInterrupt()
恢复中断状态
类比说明(食堂排队场景)
点餐时发现没菜:
你(线程)在食堂点餐发现没菜(条件不满足),把座位(锁)让给其他人(释放锁),排到“等菜队伍”(Condition 队列)中等候.等待厨师补菜:
厨师(其他线程)补菜后大喊“菜来了!”(调用signal()
),你会被从“等菜队伍”转移到“取餐队伍”(同步队列)。这时你需要重新抢座位(锁)才能开始就餐(继续执行)
signal()
调用 Condition 的 signal() 方法,会唤醒在等待队列中等待时间最⻓的节点, 也就是首节点,在唤醒节点之前,会先把节点移到同步队列里面。
调用这个方法的前置条件是当前线程必须获取了锁,可以看到 signal() 方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。
看看 signal 方法, 也就是通知方法,代码很简单
public final void signal() {
if (!isHeldExclusively()) // 当前线程必须是获取了锁的线程
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 唤醒首节点
}
接着获取等待队列的首节点,调用 doSignal 执行唤醒
private void doSignal(Node first) {
// 将条件队列中的节点转移到同步队列(Sync Queue)以唤醒线程
do {
if ( (firstWaiter = first.nextWaiter) == null) // 更新头节点
lastWaiter = null; // 重置尾节点
first.nextWaiter = null; // 断开链接
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
我们看看是怎么唤醒的? 可以看到,这里是一个循环,循环内部执行的操作就是:
-
更新头节点:将
firstWaiter
(条件队列的头部指针)指向当前节点的下一个节点first.nextWaiter
。 -
重置尾节点:若
first
是队列的最后一个节点(nextWaiter == null
),则清空lastWaiter
,保持队列一致性. -
断开链接:
first.nextWaiter = null
将当前节点从条件队列移除,避免循环引用
循环退出的条件就是: 某个节点成功转移(transferForSignal(first)
返回 true
)或者队列遍历结束(无更多节点可处理)。
我们看看它是怎么移动的呢?
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
//如果节点状态无法改变,说明已经被cancel
return false;
Node p = enq(node); // 线程安全地移动等待队列的头节点到同步队列
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
通过调用同步器的 enq(Node node) 方法,等待队列中的头节点会被线程 安全地移动到同步队列。这个方法我们之前看过,就不具体看了 。
当节点移动到同步队列后,再使用 LockSupport 唤醒该节点的线程。被唤醒后的线程,会从 await() 方法中的 while 循环中退出 (也就是 isOnSyncQueue(Node node) 方法 返回 true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状 态的竞争中。
最后 ,成功获取同步状态 (或者说锁) 之后,被唤醒的线程会从调用的 await() 方法中返回,这个时候线程已经成功地获取了锁。
signalAll()
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter; //记录当前节点的下一个节点 next
first.nextWaiter = null;//断开当前节点的链接,防止残留引用干扰遍历
transferForSignal(first);//将节点传入同步队列,唤醒对应线程
first = next;//移动至下一个节点
} while (first != null);
}
看看 Condition的signalAll()方法,它其实就相当于对等待队列中的每个节点 都执行一次 signal() 方法,效果就是把等待队列中所有节点全部移动到同步队 列当中,并且唤醒每个节点的线程。
总结
这一节主要介绍了Java并发包中和锁相关的API和组件,也通过示例展示了这些API和组件的使用 方式以及需要注意的地方,并且在此基础上我们一起详细地剖析了队列同步器、重入锁、读写锁以及 Condition等 API和组件的实现细节。只有真正理解了这些API和组件的实现细节才能够更加准确地运用它们。