详见:详解 Condition的await和signal 等待/通知机制
从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制;
而Condition与Lock配合完成等待通知机制;
前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持;
一、Condition实现原理
1、等待队列实现原理:
建一个condition对象是通过lock.newCondition(),
而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类;
在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。
ConditionObject中有两个成员变量:firstWaiter、lastWaiter:
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
可以看出来 ConditionObject 通过持有等待队列的头尾指针来管理等待队列。
Node类复用了在AQS中的Node类,其节点状态和相关属性可以去看AQS的实现;
Node类有这样一个属性:
//后继节点
Node nextWaiter;
等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。
- 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2....Thread-8;
- 等待队列是一个单向队列。
可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。
而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,
而并发包中的Lock拥有一个同步队列和多个等待队列
2、await实现原理:
当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。
2.1、是怎样将当前线程添加到等待队列中去的?
通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。
2.2、释放锁的过程?
调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。
2.3、怎样才能从await方法退出?
当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。
当退出while循环后就会调用acquireQueued(node, savedState)
,这个方法在介绍AQS的底层实现时说过了,若感兴趣的话可以去看这篇文章,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。
3、signal、signalAll实现原理:
调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。
调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。
signalAll与signal方法的区别体现在:doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,该方法只不过实现等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。
4、两者结合思考:
通过使用condition提供的await和signal/signalAll方法就可以实现等待通知机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”。
这个图很重要。可以说理解、记住这张图即可!