Condition为我们提供了可以实现等待/通知模式。例如生产消费者模式,当生产者生产的产品达到了最大库存,则
生产者会停止生产并且系统会通知消费者来进行消费;当消费者将商品全部消费完了,则消费者会停止消费等待生
产者生产新的商品。
可以看到Condition是一个接口,定义了实现等待/通知模式的抽象方法,具体如何实现等待、通知需要具体的实现
类去实现。在AbstractQueuedSynchronizer中为我们提供了ConditionObject的内部类,它就是Condition的具体
实现。在文章中如果没有特殊说明,我们所说的Condition就是指的ConditionObject。
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的
锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是
依赖Lock对象的。
Condition的使用方式比较简单,需要注意在调用方法前获取锁,如下代码所示:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author : Jhon
* @Description: TODO
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class ConditionTest
{
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void conditonAwait()
{
lock.lock();
try
{
System.out.println(Thread.currentThread().getName() + "获取到了锁");
System.out.println(Thread.currentThread().getName() + "等待信号");
condition.await();
System.out.println(Thread.currentThread().getName() + "接收到了信号");
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
public void conditionSignal()
{
lock.lock();
try
{
System.out.println(Thread.currentThread().getName() + "获取到了锁");
System.out.println(Thread.currentThread().getName() + "发送信号");
condition.signal();
System.out.println(Thread.currentThread().getName() + "发送了信号");
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
public static void main(String[] args)
{
ConditionTest conditionTest = new ConditionTest();
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(() -> {
conditionTest.conditonAwait();
});
executorService.submit(() -> {
conditionTest.conditionSignal();
});
executorService.shutdown();
}
}
控制台输出:
如示例代码所示,一般都会将Condition对象作为成员变量。在调用await方法时,会释放当前线程持有的锁并在
此等待。而其他线程在调用signal方法时,会通知当前线程后,当前线程采用await方法返回,返回时当前线程已
经获取到了锁。
实现原理
下面我们就以AQS中的内部类ConditionObject为入口,来介绍Condition的实现原理。下面将分析Condition的实
现,主要包括:同步队列、等待队列、等待、通知以及同步队列和等待队列的转换。
同步队列
同步队列在 AQS原理及源码分析 一文中的独占锁(加锁过程)做过说明。同步队列是一个FIFO的队列,在队列中
的每个节点都包含了一个线程引用和节点的同步状态,同时队列中的每个节点维护了它的前驱和后继节点关系,同
步队列基本结构如下图:
当线程获取锁时,如果同步队列已经存在,则会将当前线程构建成AQS中的Node对象,并将持有当前线程的
Node对象插入到同步队列的尾部,并维护好同步队列原尾结点的prev和next关系,同时将同步队列的尾结点指针
(tail)指向新加入的尾节点。
等待队列
等待队列也是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用和节点的等待状态,同时等待队列中
的每个节点只维护了它和后继等待节点关系。 一个Condition包含一个等待队列,Condition拥有首节点
(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造成Node对象
的节点,并将节点从尾部加入等待队列,等待队列的基本结构如图所示。
如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节
点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,
也就是说该过程是由锁来保证线程安全的。
等待队列和同步队列都是AQS中的Node对象,都包含了线程的引用和等待状态,等待队列中节点入队时,等待状
态为-2而同步队列中节点入队时,节点状态为-1。
等待
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态
变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会把当前线程构建成节点插入到等待
队列的尾部,同时释放锁,唤醒同步队列中的后继节点,然后当前线程就会park住进入等待状态,await方法的示意图如下:
addConditionWaiter方法主要是将当前线程加入到等待队列的尾部,fullyRelease方法是释放同步队列中当前线程
持有的锁,并唤醒同步队列中的后继节点,isOnSyncQueue方法是判断等待队列中持有当前线程的节点状态,如
果是等待状态,则park住线程,等待唤醒。
通知
调用Condition的signal()方法,会把等待队列的头节点从等待队列移动到同步队列的尾部,保证节点唤醒后再次去
竞争锁资源。signal方法源码为:
调用Condition的signal()方法必须是当前线程获取到了锁,可以看到如果当前线程没有获取到锁,会直接抛出
IllegalMonitorStateException异常。接着获取等待队列的头节点,调用 doSignal 方法将头结点移动到同步队列的
尾部,doSignal 方法源码为:
doSignal 方法主要是将头结点从等待队列中移除,并调用 transferForSignal 方法将节点移动到同步队列中,
transferForSignal 源码为:
transferForSignal 方法主要做了两件事情:
-
更改节点状态,由等待状态(CONDITION=-2)更改为初始化状态(0)
-
通过enq将节点插入到同步队列的尾部
signal 方法的示意图如下:
可以看到只有节点重新进入了同步队列后才有机会使得等待线程被唤醒,即从await方法中的
LockSupport.park(this)
方法中返回,从而才有机会使得调用await方法的线程成功退出。
总结
Condition的等待/通知模式,前提条件是当前线程必须获取到了锁,也就是同步队列的头结点。采用同步队列和等
待队列来回切换实现的。当线程需要等待时,AQS会将同步队列的头结点(持有锁的节点线程)移动到等待队列的
尾结点,并阻塞该线程。当线程通知时,AQS会将等待队列的头结点移动到同步队列的尾结点,保证等待的线程能
有机会重新去竞争锁,从阻塞状态被唤醒。