LockSupport 和 Condition接口
一、概述
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上)。
主要包括 wait()
、wait(long timeout)
、notify()
、notifyAll()
方法,这些方法与synchronized同步关键字配合,可以实现 等待/通知模式。
Object的等待/通知模式如下:
Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式,但这两者在使用方式以及功能特性上有差别。
Object监视器方法与Condition接口对比如下图:
二、Condition 接口与示例
1. 使用示例
Condition 定义了等待/通知两种类型的方法,Condition对象由Lock.newCondition()
方法创建出来。
注意:
当前线程调用等待/通知两种类型方法时,需要提前获取到Condition对象关联的锁。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock(); //1.先获取锁
try {
condition.await(); //2.让当前线程进入等待队列,同时会释放持有的锁。
} finally {
lock.unlock(); //3.释放锁
}
}
public void conditionSignal() throws InterruptedException {
lock.lock(); //1.先获取锁
try {
condition.signal(); //2.唤醒等待队列中头节点线程,使之进入AQS同步队列。
} finally {
lock.unlock(); //3.释放锁
}
}
2. Condition 接口
Condition 接口内的方法主要分为两部分:
- 以
await
开头的方法使当前线程进入等待队列; - 以
signal
开头的方法使当前线程从等待队列进入到AQS的同步队列中;
该接口的应用可以参考:ArrayBlockingQueue源码分析
三、Condition 实现原理
ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部类。每个Condition对象都包含着一个队列(称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
说明:
AbstractQueuedSynchronizer
内维护的队列称为: 同步队列
Condition
内维护的队列称为: 等待队列
下面分析Condition的实现,分为三部分:
- 等待队列
- 等待
- 通知
1. 等待队列
- 等待队列是一个FIFO的队列。
- 队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程。
- 如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态 (节点的定义复用了同步器中节点的定义)。
- 一个 Condition 包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。
当前线程调用 Condition.await()
方法后,将会以当前线程构造节点,并将节点从尾部加入等待队列。加入尾部的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
等待队列的基本结构如图所示:
Object 和 Lock 中同步队列、等待队列个数的差异:
- 在 Object 的监视器模型上,一个对象拥有 一个同步队列 和 一个等待队列。
- Lock(更确切地说是同步器)拥有 一个同步队列 和 多个等待队列,其对应关系如图所示。
2. 等待
调用以 await
开头的方法(如Condition.await()
方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
// AbstractQueuedSynchronizer.ConditionObject.class
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 当前线程加入等待队列
Node node = addConditionWaiter();
// 释放同步状态,也就是释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
如果从队列的角度去看,当前线程加入Condition 等待队列的过程 如图示。
3. 通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
Condition的signal()方法,如代码如下所示:
public final void signal() {
if (!isHeldExclusively()) //1.校验是否获取锁(在调用signal()方法的前提是当前线程必须持有锁)
throw new IllegalMonitorStateException();
Node first = firstWaiter; //2.获取等待队列的首节点
if (first != null)
doSignal(first); //3.将等待队列中的首节点移动到同步队列中,并唤醒该节点中的线程。
}
调用 signal()
方法有如下3步骤:
- 前置条件: 调用该方法前,当前线程必须获取了锁。因此进行了isHeldExclusively()检查。
- 接着获取等待队列的首节点。
- 将其移动到同步队列并使用LockSupport唤醒节点中的线程。
节点从等待队列移动到同步队列的过程如图所示。
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node) 方法返回true,节点已经在同步队列中),进而调用同步器的 acquireQueued() 方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
Condition的 signalAll() 方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
4. Condition 的应用
该接口的应用可以参考:ArrayBlockingQueue源码分析
四、LockSupport 类
LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。
- 以
park
开头的方法用来阻塞当前线程。 - 以
unpark(Thread thread)
方法来唤醒一个被阻塞的线程。
五、参考
- 《Java并发编程的艺术》