上一篇我们对ReentrantLock的API进行了详细的分析,相比于synchronized单个条件等待通知,ReentrantLock的多条件支持是核心优势之一,能进行更精细的多线程并发协同控制,而这核心优势依赖于与Condition接口的配合使用。本来就来进一步学习Condition接口的相关API。
认识Condition接口
要用ReentrantLock实现多条件并发协同,必须配合Condition接口使用,Condition接口是用于线程间协调通信的核心工具,下面我们就来分析下Condition接口的的相关API。
代替传统wait()/notify()
/**
* {@code Condition} factors out the {@code Object} monitor
* methods ({@link Object#wait() wait}, {@link Object#notify notify}
* and {@link Object#notifyAll notifyAll}) into distinct objects to
* give the effect of having multiple wait-sets per object, by
* combining them with the use of arbitrary {@link Lock} implementations.
* Where a {@code Lock} replaces the use of {@code synchronized} methods
* and statements, a {@code Condition} replaces the use of the Object
* monitor methods.
*/
Condition 将Object的监视器方法,Object#wait() 、Object#notify 和 Object#notifyAll分解为独立对象,通过与任意Lock接口的实现结合, 实现每个对象拥有多个等待集的效果。当Lock接口替代了synchronized方法和语句时,Condition 则替代了 Object 的监视器方法。
条件变量
/* <p>Conditions (also known as <em>condition queues</em> or
* <em>condition variables</em>) provide a means for one thread to
* suspend execution (to "wait") until notified by another
* thread that some state condition may now be true. Because access
* to this shared state information occurs in different threads, it
* must be protected, so a lock of some form is associated with the
* condition. The key property that waiting for a condition provides
* is that it <em>atomically</em> releases the associated lock and
* suspends the current thread, just like {@code Object.wait}.
*/
Conditions(也称为条件队列或 条件变量)提供了一种机制,使一个线程可暂停执行(进入“等待”状态),直到另一线程通知其某些状态条件可能已为真。由于共享状态信息在不同线程中被访问,必须通过某种形式的锁进行保护,因此条件总与锁关联。等待条件的关键特性是:它能原子性地释放关联锁并挂起当前线程,类似Object.wait的行为。
/* <p>A {@code Condition} instance is intrinsically bound to a lock.
* To obtain a {@code Condition} instance for a particular {@link Lock}
* instance use its {@link Lock#newCondition newCondition()} method.
*/
Condition实例本质上绑定到一个锁。为特定 Lock实例获取Condition实例,需使用其 Lock接口的newCondition() 方法。
/* <p>As an example, suppose we have a bounded buffer which supports
* {@code put} and {@code take} methods. If a
* {@code take} is attempted on an empty buffer, then the thread will block
* until an item becomes available; if a {@code put} is attempted on a
* full buffer, then the thread will block until a space becomes available.
* We would like to keep waiting {@code put} threads and {@code take}
* threads in separate wait-sets so that we can use the optimization of
* only notifying a single thread at a time when items or spaces become
* available in the buffer. This can be achieved using two
* {@link Condition} instances.
*/
经典用法
例如,假设有一个支持put和take方法的有界缓冲区。若在空缓冲区上尝试take,线程将阻塞直至有可用项;若在满缓冲区上尝试put,线程将阻塞直至有可用空间。通过分离put线程和take线程至不同等待集,可在缓冲区有项或空间时仅通知单一线程以优化性能。这可通过两个Condition实例实现:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(E x) throws InterruptedException {
//获取锁
lock.lock();
try {
//缓冲区满了,放不了数据,进入等待
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
//放入数据后通知读线程,缓冲区不是空的了
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
//缓冲区是空的,取不了数据,进入等待
while (count == 0)
notEmpty.await();
E x = (E) items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
//取了数据后,通知写线程缓冲区不是满的了
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
java.util.concurrent.ArrayBlockingQueue 类已提供此功能,因此无需实现此示例类。
注意事项
(1)避免使用Condition对象监视器锁
/* <p>A {@code Condition} implementation can provide behavior and semantics
* that is
* different from that of the {@code Object} monitor methods, such as
* guaranteed ordering for notifications, or not requiring a lock to be held
* when performing notifications.
* If an implementation provides such specialized semantics then the
* implementation must document those semantics.
*/
Condition 实现可提供与 Object 监视器方法不同的行为语义,例如保证通知的顺序性,或执行通知时无需持有锁(监视器方法wait()/notify()调用前必须先持有锁)。若实现提供了此类特殊语义,则必须在其文档中明确说明。
/* <p>Note that {@code Condition} instances are just normal objects and can
* themselves be used as the target in a {@code synchronized} statement,
* and can have their own monitor {@link Object#wait wait} and
* {@link Object#notify notify} methods invoked.
* Acquiring the monitor lock of a {@code Condition} instance, or using its
* monitor methods, has no specified relationship with acquiring the
* {@link Lock} associated with that {@code Condition} or the use of its
* {@linkplain #await waiting} and {@linkplain #signal signalling} methods.
* It is recommended that to avoid confusion you never use {@code Condition}
* instances in this way, except perhaps within their own implementation.
* <p>Except where noted, passing a {@code null} value for any parameter
* will result in a {@link NullPointerException} being thrown.
*/
Condition 实例本质是普通对象,可作为 synchronized 语句的目标对象,并调用其监视器方法 wait() 和 notify();获取 Condition 实例的监视器锁或调用其监视器方法,与获取关联的 Lock 或使用其等待/唤醒方法无必然联系;为避免混淆,建议切勿以该方式使用 Condition 实例(除非在其自身实现内部)。
重要规范:除非特别说明,任何参数传递 null 值均会抛出 NullPointerException。
(2)虚假唤醒的处理
/*
* <p>When waiting upon a {@code Condition}, a "<em>spurious
* wakeup</em>" is permitted to occur, in
* general, as a concession to the underlying platform semantics.
* This has little practical impact on most application programs as a
* {@code Condition} should always be waited upon in a loop, testing
* the state predicate that is being waited for. An implementation is
* free to remove the possibility of spurious wakeups but it is
* recommended that applications programmers always assume that they can
* occur and so always wait in a loop.
*/
等待 Condition 时允许发生虚假唤醒(spurious wakeup),这是对底层平台语义的妥协。应在循环中等待 Condition,并持续检测被等待的状态条件;实现方虽可消除虚假唤醒可能性,但开发者应默认其可能发生并始终采用循环等待策略。
(3)三种等待形式的差异
/* <p>The three forms of condition waiting
* (interruptible, non-interruptible, and timed) may differ in their ease of
* implementation on some platforms and in their performance characteristics.
* In particular, it may be difficult to provide these features and maintain
* specific semantics such as ordering guarantees.
* Further, the ability to interrupt the actual suspension of the thread may
* not always be feasible to implement on all platforms.
*/
可中断、不可中断及超时等待在实现难度和性能特性上可能不同:平台实现可能导致特性支持差异(如排序保证);线程挂起的中断能力并非所有平台均可实现。
(3)实现灵活性要求
/* <p>Consequently, an implementation is not required to define exactly the
* same guarantees or semantics for all three forms of waiting, nor is it
* required to support interruption of the actual suspension of the thread.
*/
无需为三种等待形式提供完全相同的语义保证;不强制要求支持线程挂起的中断操作。
(4)文档规范义务
/* <p>An implementation is required to
* clearly document the semantics and guarantees provided by each of the
* waiting methods, and when an implementation does support interruption of
* thread suspension then it must obey the interruption semantics as defined
* in this interface.
*/
实现方必须明确记录:每种等待方法的具体语义与保证;若支持线程挂起中断,则需遵循本接口定义的中断语义。
(5)中断响应优先级
/* <p>As interruption generally implies cancellation, and checks for
* interruption are often infrequent, an implementation can favor responding
* to an interrupt over normal method return. This is true even if it can be
* shown that the interrupt occurred after another action that may have
* unblocked the thread. An implementation should document this behavior.
*/
中断通常意味着取消操作,且中断检查频率较低,因此:实现应优先响应中断而非正常返回;即使中断发生在解除线程阻塞的操作之后,仍需优先响应;此类行为必须在文档中明确说明。
Condition接口API详解
(1)await()
/**
* Causes the current thread to wait until it is signalled or
* {@linkplain Thread#interrupt interrupted}.
*
* <p>The lock associated with this {@code Condition} is atomically
* released and the current thread becomes disabled for thread scheduling
* purposes and lies dormant until <em>one</em> of four things happens:
* <ul>
* <li>Some other thread invokes the {@link #signal} method for this
* {@code Condition} and the current thread happens to be chosen as the
* thread to be awakened; or
* <li>Some other thread invokes the {@link #signalAll} method for this
* {@code Condition}; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of thread suspension is supported; or
* <li>A "<em>spurious wakeup</em>" occurs.
* </ul>
*
* <p>In all cases, before this method can return the current thread must
* re-acquire the lock associated with this condition. When the
* thread returns it is <em>guaranteed</em> to hold this lock.
*
* <p>If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting
* and interruption of thread suspension is supported,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared. It is not specified, in the first
* case, whether or not the test for interruption occurs before the lock
* is released.
*
* <p><b>Implementation Considerations</b>
*
* <p>The current thread is assumed to hold the lock associated with this
* {@code Condition} when this method is called.
* It is up to the implementation to determine if this is
* the case and if not, how to respond. Typically, an exception will be
* thrown (such as {@link IllegalMonitorStateException}) and the
* implementation must document that fact.
*
* <p>An implementation can favor responding to an interrupt over normal
* method return in response to a signal. In that case the implementation
* must ensure that the signal is redirected to another waiting thread, if
* there is one.
*
* @throws InterruptedException if the current thread is interrupted
* (and interruption of thread suspension is supported)
*/
void await() throws InterruptedException;
调用该方法使当前线程进入等待状态,直到被唤醒(signal)或 interrupt 中断。调用此方法时,与该 Condition关联的锁会被原子性地释放,当前线程进入调度禁用状态并保持休眠,直到发生以下四种情况之一:
(1)其他线程调用此Condition的signal方法,且当前线程被选中唤醒;
(2)其他线程调用此Condition的signalAll方法;
(3)其他线程interrupt 中断当前线程,且支持中断线程挂起操作;
(4)发生虚假唤醒(spurious wakeup)。
无论何种情况,该方法返回前线程必须重新获取与此条件关联的锁。当线程返回时,保证持有该锁。
若当前线程:
进入此方法时已被设置中断状态;等待过程中被interrupt 中断且支持中断线程挂起,则会抛出InterruptedException并清除线程中断状态。 对于第一种情况,未规定锁释放前是否进行中断检查。
实现注意事项:
调用此方法时,假定当前线程持有与该Condition关联的锁。具体实现需验证此条件,若未满足则应定义响应行为(通常抛出如 IllegalMonitorStateException} 的异常),且必须记录此要求。
实现可优先响应中断而非正常唤醒(响应 signal)。此时必须确保若存在其他等待线程,信号会被重定向至另一个等待线程。
(2)void awaitUninterruptibly()
调用该方法使当前线程进入等待状态,直到被唤醒(signal)。唤醒条件除了不能被中断,其他的和await()方法一样。
**和await()方法最大的区别是即使线程进入时已中断或者等待过程中被interrupt中断,线程仍会继续等待直到被唤醒。当最终从该方法返回时,线程的中断状态将保持不变。**适用于必须完成关键代码段且不允许中断的并发控制场景。
(3)long awaitNanos(long nanosTimeout)
使当前线程进入等待状态,直到被唤醒(signal)、中断,或达到指定的等待时间。和await()方法相比,唤醒条件多了一个,达到指定的等待时间。
返回值是对剩余等待时间的估算(原超时值减去实际等待时间,单位:纳秒),若>0 时可作为后续重试参数,若超时则返回 ≤0 的值。此值可用于确定在虚假唤醒后是否需要重新等待及等待时长,典型用法如下:
boolean aMethod(long timeout, TimeUnit unit) throws InterruptedException {
long nanosRemaining = unit.toNanos(timeout);
lock.lock();
try {
while (!conditionBeingWaitedFor()) {
if (nanosRemaining <= 0L) return false;
nanosRemaining = theCondition.awaitNanos(nanosRemaining); // 重新计算剩余时间
}
return true;
} finally {
lock.unlock();
}
}
设计说明:采用纳秒参数可避免剩余时间报告的精度损失,防止重试场景下总等待时间系统性短于预设值。此方法实现了精确的纳秒级超时控制,通过返回值重试机制有效解决虚假唤醒问题,是构建高精度超时等待的核心API。
(4)boolean await(long time, TimeUnit unit)
使当前线程等待,直到收到信号、被中断或指定的等待时间耗尽。该方法在行为上等同于:awaitNanos(unit.toNanos(time)) > 0。
- @param time 最大等待时间
- @param unit time参数的时间单位
- @return 如果等待时间在方法返回前可检测地耗尽,则返回false,否则返回true。
(5)boolean awaitUntil(Date deadline)
使当前线程等待,直到收到信号、被中断或指定的截止时间到达。与其他await方法的主要区别体现在等待机制和时间控制方式上:
(1)使用绝对时间(具体截止日期)作为等待条件;await(long time, TimeUnit unit)/awaitNanos():使用相对时间(从调用时刻开始的时长)作为等待条件。
(2)awaitUntil()返回false表示截止时间已到达,其他await方法返回false通常表示等待超时。
(6)void signal()
/**
* Wakes up one waiting thread.
*
* <p>If any threads are waiting on this condition then one
* is selected for waking up. That thread must then re-acquire the
* lock before returning from {@code await}.
*
* <p><b>Implementation Considerations</b>
*
* <p>An implementation may (and typically does) require that the
* current thread hold the lock associated with this {@code
* Condition} when this method is called. Implementations must
* document this precondition and any actions taken if the lock is
* not held. Typically, an exception such as {@link
* IllegalMonitorStateException} will be thrown.
*/
void signal();
调用该方法唤醒一个等待线程。如果有线程正在此条件上等待,则选择一个线程被唤醒。被唤醒的线程必须重新获取锁后才能从await()方法返回。
实现注意事项:实现可能会(并且通常需要)要求调用此方法时当前线程必须持有与该Condition关联的锁。实现必须记录此前提条件,并在未持有锁时采取相应措施。通常情况下,会抛出IllegalMonitorStateException等异常。
(7)void signalAll()
调用该方法唤醒所有等待线程,其他同signal()方法。
总结
最后用表格总结下各个方法的说明
方法名 | 功能描述 | 与Object方法的区别 | 实现特性 |
---|---|---|---|
void await() | 释放锁并进入等待,直到被signal或中断 | 对应Object.wait(),但需配合Lock使用 | 支持中断响应,需用while循环检查条件防止虚假唤醒 |
boolean await(long time, TimeUnit unit) | 带超时等待,返回是否被唤醒 | 比Object.wait(long timeout)增加时间单位参数 | 超时、中断或被唤醒时返回false/true |
boolean awaitUntil(Date deadline) | 等待直到指定截止时间 | Object无对应方法,Condition独有 | 直接设置绝对时间点,避免时间计算 |
void awaitUninterruptibly() | 不响应中断的等待 | Object无对应方法 | 线程中断标志被设置后仍保持阻塞 |
void signal() | 随机唤醒一个等待线程 | 对应Object.notify() | 需持有锁调用,唤醒线程需重新竞争锁 |
void signalAll() | 唤醒所有等待线程 | 对应Object.notifyAll() | 适用于多条件队列场景 |