在没有 Lock 之前,我们使用 synchronized
来控制同步,配合 Object 的 #wait()
、#notify()
等一系列方法可以实现等待 / 通知模式。
而Object 的 #wait()
、#notify()的缺点主要在于一个锁只支持一个等待队列,如果一个锁上的等待条件又多个,一个条件满足后,会唤醒等待队列中所有的线程,而等待队列中有些线程是在等待其他的条件,也就是“
过早唤醒问题”,这就造成了一些线程被不必要的唤醒增加线程切换次数影响性能。
所以引入了Condition,它能够在一个锁上创建多个条件队列,解决以上问题。
代码如下:
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter; // 头节点
/** Last node of condition queue. */
private transient Node lastWaiter; // 尾节点
public ConditionObject() {
}
// ... 省略内部代码
}
从上面代码可以看出,ConditionObject 拥有首节点(firstWaiter
),尾节点(lastWaiter
)。当前线程调用 #await()
方法时,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:
-
Node 里面包含了当前线程的引用。Node 定义与 AQS 的 CLH 同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized 的 Node 静态内部类)。
- ConditionObject 的队列结构比 CLH 同步队列的结构简单些,是一个FIFO单向链表。
以下是AQS队列和Condition队列的出入结点的示意图,转自 https://blog.csdn.net/coslay/article/details/45217069
可以通过这几张图看出线程结点在两个队列中的出入关系和条件。
I.初始化状态:AQS等待队列有3个Node,Condition队列有1个Node(也有可能1个都没有)
II.节点1执行Condition.await()
1.将head后移
2.释放节点1的锁并从AQS等待队列中移除
3.将节点1加入到Condition的等待队列中
4.更新lastWaiter为节点1
III.节点2执行signal()操作
5.将firstWaiter后移
6.将节点4移出Condition队列
7.将节点4加入到AQS的等待队列中去
8.更新AQS的等待队列的tail
总结:
一个线程获取锁后,通过调用 Condition 的 #await()
方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过 #isOnSyncQueue(Node node)
方法,不断自检看节点是否已经在 CLH 同步队列了,如果是则尝试获取锁,否则一直挂起。
当线程调用 #signal()
方法后,程序首先检查当前线程是否获取了锁,然后通过#doSignal(Node first)
方法唤醒CLH同步队列的首节点。被唤醒的线程,将从 #await()
方法中的 while
循环中退出来,然后调用 #acquireQueued(Node node, int arg)
方法竞争同步状态。
源码分析
http://www.iocoder.cn/JUC/sike/Condition/