当前位置 博文首页 > Shockang的博客:Condition 是什么?怎么用?

    Shockang的博客:Condition 是什么?怎么用?

    作者:[db:作者] 时间:2021-08-23 22:12

    前言

    本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

    本专栏目录结构和参考文献请见100个问题搞定Java并发

    正文

    如果大家理解了 Object.wait() 方法和 Object.notify() 方法,就能很容易地理解 Condition 对象了。

    它与 wait() 方法和 notify() 方法的作用是大致相同的。

    关于 wait 和 notify 请参考我的博客——结合JDK源码图文详解 wait 和 notify 的工作原理

    但是 wait() 方法和 notify() 方法是与 synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。

    通过 lock 接口(ReentrantLock就实现了这一接口)的 Condition newCondition 方法可以生成一个与当前重入锁绑定的 Condition 实例。

    利用 Condition 对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。

    源码(JDK8)

    /**
     * Condition 移出 Object 的 monitor 方法( wait , notify和notifyAll )成不同的对象,以得到每个对象具有多个等待集,通过将它们与任意的 Lock 实现混用。 
     * 
     * Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。
     *
     * 条件(也称为条件队列或条件变量 )为一个线程暂停执行(“wait”)提供了一种方法,直到另一个线程通知某些状态现在可能为真。 
     * 
     * 因为访问此共享状态信息发生在不同的线程中,所以它必须被保护,因此某种形式的锁与该条件相关联。 
     * 
     * 等待条件的关键属性是它原子地释放相关的锁并挂起当前线程,就像 Object.wait 。
     * 
     * 一个Condition实例本质上绑定到一个锁。 要获得特定Condition实例的Condition实例,请使用其newCondition()方法。
     * 
     * 例如,假设我们有一个有限的缓冲区,它支持put和take方法。 
     * 
     * 如果在一个空的缓冲区尝试一个take ,则线程将阻塞直到一个项目可用; 
     * 
     * 如果put试图在一个完整的缓冲区,那么线程将阻塞,直到空间变得可用。 
     * 
     * 我们希望在单独的等待集中等待put线程和take线程,以便我们可以在缓冲区中的项目或空间可用的时候使用仅通知单个线程的优化。 
     * 
     * 这可以使用两个Condition实例来实现。
     * 
     * class BoundedBuffer {
     *   <b>final Lock lock = new ReentrantLock();</b>
     *   final Condition notFull  = <b>lock.newCondition(); </b>
     *   final Condition notEmpty = <b>lock.newCondition(); </b>
     *
     *   final Object[] items = new Object[100];
     *   int putptr, takeptr, count;
     *
     *   public void put(Object x) throws InterruptedException {
     *     <b>lock.lock();
     *     try {</b>
     *       while (count == items.length)
     *         <b>notFull.await();</b>
     *       items[putptr] = x;
     *       if (++putptr == items.length) putptr = 0;
     *       ++count;
     *       <b>notEmpty.signal();</b>
     *     <b>} finally {
     *       lock.unlock();
     *     }</b>
     *   }
     *
     *   public Object take() throws InterruptedException {
     *     <b>lock.lock();
     *     try {</b>
     *       while (count == 0)
     *         <b>notEmpty.await();</b>
     *       Object x = items[takeptr];
     *       if (++takeptr == items.length) takeptr = 0;
     *       --count;
     *       <b>notFull.signal();</b>
     *       return x;
     *     <b>} finally {
     *       lock.unlock();
     *     }</b>
     *   }
     * }
     * 
     * ( ArrayBlockingQueue类提供此功能,因此没有理由实现此示例使用类。)
     * 
     * Condition实现可以提供Object监视器方法的行为和语义,例如有保证的通知顺序,或者在执行通知时不需要锁定。 
     * 
     * 如果一个实现提供了这样的专门的语义,那么实现必须记录这些语义。
     * 
     * 需要注意的是Condition实例只是普通的对象,其本身作为一个目标synchronized语句,自己也可以调用 wait 和 notify 方法。 
     * 
     * 获取Condition实例的监视器锁或使用其监视方法与获取与该Condition相关联的Condition或使用其wait和signal方法。 
     * 
     * 建议为避免混淆,您永远不会以这种方式使用Condition实例,除了可能在自己的实现之内。
     * 
     * 除非另有说明,传递任何参数的null值将导致NullPointerException被抛出。
     * 
     * 实施注意事项
     * 
     * 当等待Condition时,允许发生“ 虚假唤醒 ”,一般来说,作为对底层平台语义的让步。 
     * 
     * 这对大多数应用程序几乎没有实际的影响,因为Condition应该始终在循环中等待,测试正在等待的状态谓词。 
     * 
     * 一个实现可以免除虚假唤醒的可能性,但建议应用程序员总是假定它们可以发生,因此总是等待循环。
     * 
     * 条件等待(可中断,不可中断和定时)的三种形式在一些平台上的易用性和性能特征可能不同。 
     * 
     * 特别地,可能难以提供这些特征并保持特定的语义,例如排序保证。 此外,中断线程实际挂起的能力可能并不总是在所有平台上实现。
     * 
     * 因此,不需要一个实现来为所有三种形式的等待定义完全相同的保证或语义,也不需要支持中断线程的实际暂停。
     * 
     * 需要一个实现来清楚地记录每个等待方法提供的语义和保证,并且当一个实现确实支持线程挂起中断时,它必须遵守该接口中定义的中断语义。
     * 
     * 由于中断通常意味着取消,并且检查中断通常是不频繁的,所以实现可以有利于通过正常方法返回来响应中断。 
     * 
     * 即使可以显示中断发生在另一个可能解除阻塞线程的动作之后,这一点也是如此。 一个实现应该记录这个行为。
     * 
     * @since 1.5
     * @author Doug Lea
     */
    
    public interface Condition {
        /**
         * 导致当前线程等到发信号或interrupted 。
         * 
         * 与此 Condition 关联的 Lock 是原子方式释放,当前线程将禁用线程调度并且休眠直到下面三件事中的一件发生:
         * 
         * 一些其它线程调用本Condition的signal()方法,当前线程碰巧被选择作为被唤醒线程;
         * 
         * 一些其他的线程调用本Condition的signalAll();
         * 
         * 发生了“虚假唤醒 ”。
         * 
         * 在所有情况下,在此方法返回之前,当前线程必须重新获取与此条件相关的锁。 
         * 
         * 当线程返回时,它保证保持此锁。
         * 
         * 如果当前线程:
         * 
         * 在进入该方法时设置了中断状态; 
         * 
         * 或者在 wait 的时候被 Thread#interrupt 中断,它会继续等待直到 signalled
         * 
         * 当它最终从这个方法返回它的中断状态就准备好了。
         * 
         * 实现的注意事项
         * 
         * 当调用此方法时,假定当前线程保持与 Condition 关联的 Lock。
         * 
         * 由执行决定是否是这种情况,如果没有,应如何回应。 
         * 
         * 通常,将抛出异常(例如IllegalMonitorStateException ),并且实现必须记录该事实。
         * 
         * @throws InterruptedException - 如果当前线程中断(支持线程中断)
         */
        void await() throws InterruptedException;
    
        /**
         * 使当前线程wait,直到 signal。 
         * 
         * 与此 Condition 关联的 Lock 是原子方式释放,当前线程将禁用线程调度并且休眠直到下面三件事中的一件发生:
         *
         * 一些其它线程调用本Condition的signal()方法,当前线程碰巧被选择作为被唤醒线程;
         *
         * 一些其他的线程调用本Condition的signalAll();
         *
         * 发生了“虚假唤醒 ”。
         * 
         * 在所有情况下,在该方法返回之前,当前线程必须重新获取与该 Condition 关联的 Lock。
         * 
         * 当线程返回时,保证持有该锁。 
         * 
         * 如果当前线程的中断状态是在它进入这个方法时设置的,或者它在等待时被中断,它将继续等待直到发出信号。
         * 
         * 当它最终从此方法返回时,它的中断状态仍将被设置。 
         * 
         * 实现的注意事项 
         * 
         * 调用此方法时,假定当前线程持有与此 Condition 关联的 Lock。
         * 
         * 这取决于执行情况,以确定是否是这种情况,如果不是,如何应对。
         * 
         * 通常,会抛出异常(例如IllegalMonitorStateException),实现必须记录该事实。
         */
        void awaitUninterruptibly();
    
        /**
         * 使当前线程等待,直到发出信号或中断,或者经过指定的等待时间。
         * 
         * 与此条件相关联的锁以原子方式释放,当前线程出于线程调度目的被禁用,并处于休眠状态,直到发生以下五种情况之一: 
         * 
         * 另一个线程为此条件调用signal方法,而当前线程恰好被选为要唤醒的线程;
         * 
         * 其他线程为此条件调用signalAll方法;
         * 
         * 其他线程中断当前线程,支持中断线程挂起;
         * 
         * 经过指定的等待时间;
         * 
         * 出现“虚假唤醒”。 
         * 
         * 在所有情况下,在该方法返回之前,当前线程必须重新获取与该 Condition 关联的 Lock。
         * 
         * 当线程返回时,保证持有该锁。 
         * 
         * 如果当前线程: 
         * 
         * 在进入此方法时设置其中断状态;
         * 
         * 或 等待时中断,支持线程挂起中断, 然后抛出InterruptedException并清除当前线程的中断状态。
         * 
         * 在第一种情况下,没有规定是否在释放锁之前进行中断测试。 
         * 
         * 该方法返回给定返回时提供的nanosTimeout值的剩余等待纳秒数的估计值,如果超时,则返回小于或等于零的值。
         * 
         * 在等待返回但等待的条件仍不成立的情况下,此值可用于确定是否重新等待以及等待多长时间。
         * 
         * 此方法的典型用途如下: 
         * 
         * boolean aMethod(long timeout, TimeUnit unit) {
         *   long nanos = unit.toNanos(timeout);
         *   lock.lock();
         *   try {
         *     while (!conditionBeingWaitedFor()) {
         *       if (nanos <= 0L)
         *         return false;
         *       nanos = theCondition.awaitNanos(nanos);
         *     }
         *     // ...
         *   } finally {
         *     lock.unlock();
         *   }
         * }}
         * 
         * 设计说明:此方法需要纳秒参数,因此以避免在报告剩余时间时出现截断错误。
         * 
         * 这样的精度损失将使程序员很难确保总等待时间不会系统性地缩短重新等待时指定的值。
         * 
         * 实现注意事项:
         * 
         * 假设当前线程持有与此关联的锁  Condition 调用此方法时。由实现者来确定这是否正确
         * 
         * 如果没有,如何回应。通常,会出现异常抛出(例如{@link IllegalMonitorStateException})和执行工作必须记录这一事实。
         * 
         * 一个实现比普通实现更倾向于响应中断方法返回对信号的响应,或指示经过的时间指定的等待时间。
         * 
         * 无论哪种情况,实施必须确保信号被重定向到另一个等待线程,如果有一个。
         * 
         * @param nanosTimeout 等待的最长时间,以纳秒为单位
         * @return nanosTimeout 值减去等待从此方法返回所花费的时间。正值可以用作对该方法的后续调用以完成等待所需时间。小于或等于零的值表示没有剩余时间。
         * @throws InterruptedException 如果当前线程被中断(支持挂线中断)
        (*/
        long awaitNanos(long nanosTimeout) throws InterruptedException;
    
        /**
         * 使当前线程等待,直到发出信号或中断,或者经过指定的等待时间。此方法在行为上等同于: 单位时间>0
         * 
         * @param time 最大等待时间
         * @param unit 时间单位
         * @return 如果在从方法返回之前检测到等待时间已过为 false,否则则是 true
         * @throws InterruptedException 如果当前线程被中断(支持挂线中断)
         */
        boolean await(long time, TimeUnit unit) throws InterruptedException;
    
        /**
         * 使当前线程等待,直到发出信号或被中断,或者指定的截止时间已过。 
         * 
         * 与此条件相关联的锁以原子方式释放,当前线程出于线程调度目的被禁用,并处于休眠状态,直到发生以下五种情况之一: 
         * 
         * 另一个线程为此条件调用signal方法,而当前线程恰好被选为要唤醒的线程;
         * 
         * 或 其他线程为此条件调用signalAll方法;
         * 
         * 或 其他线程中断当前线程,支持中断线程挂起;
         * 
         * 或 超过规定期限的;
         * 
         * 或 出现“虚假唤醒”。 
         * 
         * 在所有情况下,在该方法返回之前,当前线程必须重新获取与该条件相关联的锁。当线程返回时,保证持有该锁。 
         * 
         * 如果当前线程: 
         * 
         * 在进入此方法时设置其中断状态;
         * 
         * 或 等待时中断,支持线程挂起中断, 然后抛出InterruptedException并清除当前线程的中断状态。
         * 
         * 在第一种情况下,没有规定是否在释放锁之前进行中断测试。 返回值表示截止日期是否已过,可按如下方式使用:
         * 
         * boolean aMethod(Date deadline) {
         *   boolean stillWaiting = true;
         *   lock.lock();
         *   try {
         *     while (!conditionBeingWaitedFor()) {
         *       if (!stillWaiting)
         *         return false;
         *       stillWaiting = theCondition.awaitUntil(deadline);
         *     }
         *     // ...
         *   } finally {
         *     lock.unlock();
         *   }
         * }}
         * 
         * 实现的注意事项
         * 
         * 假设当前线程持有与此关联的锁{@code Condition}调用此方法时。由实现者来确定这是否正确
         * 
         * 如果没有,如何回应。通常,会出现异常抛出(例如{@link IllegalMonitorStateException})和执行工作必须记录这一事实。
         * 
         * 一个实现比普通实现更倾向于响应中断方法返回对信号的响应,或over表示通过在指定的截止日期。
         * 
         * 无论哪种情况,实施必须确保如果需要,信号将重定向到另一个等待线程。
         * 
         * @param deadline 等待的绝对时间
         * @return 如果返回时截止日期已过则为 false ,否则 true
         * @throws InterruptedException 如果当前线程被中断(支持挂线中断)
         */
        boolean awaitUntil(Date deadline) throws InterruptedException;
    
        /**
         * 唤醒一个正在等待的线程。 
         * 
         * 如果有任何线程正在等待此条件,则会选择一个线程进行唤醒。
         * 
         * 然后该线程必须在从wait返回之前重新获取锁。 
         * 
         * 实现的注意事项 
         * 
         * 当调用此方法时,实现可能(并且通常确实)要求当前线程持有与此条件相关联的锁。
         * 
         * 实现必须记录这个先决条件,以及在没有锁的情况下所采取的任何操作。
         * 
         * 通常,会抛出异常,例如IllegalMonitorStateException
         */
        void signal();
    
        /**
         * 唤醒所有等待的线程。 
         * 
         * 如果有线程在此条件下等待,那么它们都会被唤醒。
         * 
         * 每个线程必须重新获取锁,然后才能从wait返回。 
         * 
         * 实现的注意事项 
         *
         * 当调用此方法时,实现可能(并且通常确实)要求当前线程持有与此条件相关联的锁。
         *
         * 实现必须记录这个先决条件,以及在没有锁的情况下所采取的任何操作。
         *
         * 通常,会抛出异常,例如IllegalMonitorStateException
         */
        void signalAll();
    
    }
    

    实践

    package com.shockang.study.java.concurrent.lock;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReenterLockCondition implements Runnable {
        public static ReentrantLock lock = new ReentrantLock();
        public static Condition condition = lock.newCondition();
    
        @Override
        public void run() {
            try {
                lock.lock();
                condition.await();
                System.out.println("Thread is going on");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ReenterLockCondition tl = new ReenterLockCondition();
            Thread t1 = new Thread(tl);
            t1.start();
            Thread.sleep(2000);
            //通知线程t1继续执行
            lock.lock();
            condition.signal();
            lock.unlock();
        }
    }
    

    控制台输出

    Thread is going on
    

    说明

    第 8 行代码通过 lock 生成一个与之绑定的 Condition 对象。

    第 14 行代码要求线程在 Condition 对象上进行等待。

    第 30 行代码由主线程 main 发出通知,告知等待在 Condition 上的线程可以继续执行了

    与 Object.wait() 方法和 notify() 方法一样,当线程使用 Condition.await() 方法时,要求线程持有相关的重入锁,在 Condition.await() 方法调用后,这个线程会释放这把锁

    同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。

    在 signal() 方法调用后,系统会从当前 Condition 对象的等待队列中唤醒一个线程。

    一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。

    因此,在 signal() 方法调用之后,一般需要释放相关的锁,让给被唤醒的线程,让它可以继续执行。

    比如,在本例中,第 31 行代码就释放了重入锁,如果省略第 31 行,那么,虽然已经唤醒了线程 t1 ,但是由于它无法重新获得锁,因而也就无法真正的继续执行。

    cs