等待/通知模式的一个辅助类Condition
Condition简介
在java中,对于任意一个java对象,它都拥有一组定义在java.lang.Object上监视器方法,包括wait(),wait(long timeout),notify(),notifyAll(),这些方法配合synchronized关键字一起使用可以实现等待/通知模式。
同样,Condition接口提供了类似Object监视器的方法,通过与Lock配合来实现等待/通知模式。
为了更好的了解Condition的特性,我们来对比一下两者的使用方式以及功能特性:
对比项 | Object监视器 | Condition |
前置条件 | 获取对象的锁 | 调用Lock.lock获取锁,调用Lock.newCondition获取Condition对象 |
调用方式 | 直接调用,比如object.notify() | 直接调用,比如condition.await() |
等待队列的个数 | 一个 | 多个 |
当前线程释放锁进入等待状态 | 支持 | 支持 |
当前线程释放锁进入等待状态,在等待状态中不断响中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态直到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition使用示例
实现一个简单的有界队列,队列为空时,队列的删除操作将会阻塞直到队列中有新的元素,队列已满时,队列的插入操作将会阻塞直到队列出现空位。
不难看出,Condition的使用方式是比较简单的,需要注意的是使用Condition的等待/通知需要提前获取到与Condition对象关联的锁,Condition对象由Lock对象创建。
以上述示例中的add(T object)为例,详细描述一下Condition等待/通知的整个过程:
- 获取锁,确保对数据数据修改的安全性;
- 数组元素的个数等于数组的长度时,调用notFull.await(),插入线程释放锁进入等待;
- 数组未满,添加元素到数组中,调用notEmpty.signal()通知等待在notEmpty上的线程,数组中有新的元素可以操作。
总的来说,Condition的等待/通知使用方式大体上跟经典的Object监视器上的等待/通知是非常类似的。
Condition实现分析
Condition api
Condition提供以下接口以供实现:
void await() throws InterruptedException
当前线程进入等待状态,直到被通知(signal)或者被中断时,当前线程进入运行状态,从await()返回;
void awaitUninterruptibly()
当前线程进入等待状态,直到被通知,对中断不做响应;
long awaitNanos(long nanosTimeout) throws InterruptedException
在接口1的返回条件基础上增加了超时响应,返回值表示当前剩余的时间,如果在nanosTimeout之前被唤醒,返回值 = nanosTimeout - 实际消耗的时间,返回值 <= 0表示超时;
boolean await(long time, TimeUnit unit) throws InterruptedException
同样是在接口1的返回条件基础上增加了超时响应,与接口3不同的是:
可以自定义超时时间单位;
返回值返回true/false,在time之前被唤醒,返回true,超时返回false。
boolean awaitUntil(Date deadline) throws InterruptedException
当前线程进入等待状态直到将来的指定时间被通知,如果没有到指定时间被通知返回true,否则,到达指定时间,返回false;
void signal()
唤醒一个等待在Condition上的线程;
void signalAll()
唤醒等待在Condition上所有的线程。
Condition具体实现分析
ConditionObject是Condition在java并发中的具体的实现,它是AQS的内部类。因为Condition相关操作都需要获取锁,所以作为AQS的内部类也比较合理。接下来就以ConditionObject的等待队列、等待、通知为切入点分析ConditionObject的具体实现。
等待队列
ConditionObject的等待队列是一个FIFO队列,队列的每个节点都是等待在Condition对象上的线程的引用,在调用Condition的await()方法之后,线程释放锁,构造成相应的节点进入等待队列等待。其中节点的定义复用AQS的Node定义。
- ConditionObject包含等待队列的首节点firstWaiter和尾节点lastWaiter;
- 线程调用await()方法时,调用addConditionWaiter()方法入队:
- step1:将线程构造成Node;
- step2:将Node加入到等待队列中。
从队列相关操作的具体实现可以知道等待队列的基本结构如下图所示:
插入节点只需要将原有尾节点的nextWaiter指向当前节点,并且更新尾节点。更新节点并没有像AQS更新同步队列使用CAS是因为调用await()方法的线程必定是获取了锁的线程,锁保证了操作的线程安全。
注:AQS实质上拥有一个同步队列和多个等待队列,具体对应关系如下图所示:
等待
调用Condition的await开头的系列方法,当前线程进入等待队列等待,那么Condition的等待实质是await系列方法的具体实现。
具体执行流程如下:
- 调用addConditionWaiter将当前线程加入等待队列;
- 调用fullRelease释放当前线程节点的同步状态,唤醒后继节点;
- 线程进入等待状态;
- 线程被唤醒后,从while循环中退出,调用acquireQueued尝试获取同步状态;
- 同步状态获取成功后,线程从await方法返回。
唤醒
调用Condition的signal()方法将会唤醒在等待队列中的首节点,该节点也是到目前为止等待时间最长的节点。
1、signal实现
- step1:前置检查,判断当前线程是否是获取了锁的线程,如果不是抛出异常IllegalMonitorStateException,否则,执行step2;
- step2:取得等待队列的头结点,头结点不为空执行doSignal,否则,signal结束
可以看出,doSignal方法是整个signal方法实现的核心,它完成了将线程从唤醒的所有操作。
2、doSignal实现
整个doSignal完成了这两个操作:调用transferForSignal将节点从等待队列移动到同步队列,并且,将该节点从等待队列删除。
3、transferForSignal实现
- step1:将节点waitStatus设置为0,设置成功执行step2,否则返回false;
- step2:调用enq方法将该节点加入同步队列;
- step3:使用LockSuppor.unpark()方法唤醒该节点的线程。
Condition的signalAll()方法,将等待队列中的所有节点全部唤醒,相当于将等待队列中的每一个节点都执行一次signal()。整个signal系列方法将线程从等待队列移动到同步队列可以总结为下图: