并发编程原理与实战(十九)Condition接口API全面解析

上一篇我们对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 &quot;wait&quot;) 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 &quot;<em>spurious
 * wakeup</em>&quot; 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 &quot;<em>spurious wakeup</em>&quot; 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()适用于多条件队列场景
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值