前言

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

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

正文

为了支持多线程之间的协作,JDK 提供了两个非常重要的接口:线程等待 wait() 方法和通知 notify() 方法。

为什么这两个方法并不是在 Thread 类中的,而是在 Object 类里面?

一方面意味着任何对象都可以调用这两个方法。
另一方面, 无论是 wait() 方法或者 notify() 方法都需要首先获得目标对象的一个监视器(Monitor)。
如果定义在 Thread 类里面,是不方便获取目标对象的 Monitor 的;定义在 Object 类里面,只需要 synchronized(this) 就能获取当前对象的 Monitor。

为什么需要监视器(Monitor)?

之所以需要监视器(Monitor),本质上是确保同一时间只会有一个线程操作同一个物理区域(JVM内存中的Object),同时它也保障了不同线程之间的有效通信。

当在一个对象实例上调用 wait() 方法后,当前线程就会在这个对象上等待。

这是什么意思呢?

比如,在线程 A 中,调用了 obj.wait() 方法,那么线程 A 就会停止继续执行,转为等待状态。

关于线程的状态请参考我的这篇博客——Java 中线程状态有哪些?

等待到何时结束呢?

线程 A 会一直等到其他线程调用了 obj.notify() 或者 obj.notifyAll() 方法为止。

这时,object 对象俨然成了多个线程之间的有效通信手段。

wait() 方法和 notify() 方法究竟是如何工作的呢?

如果一个线程调用了 object.wait() 方法,那么它就会进入 object 对象的等待队列(wait set)。

这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。

当 object.notify() 方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。

这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。

notify和notifyAll

这里还需要强调一点,object.wait() 方法并不能随便调用。

它必须包含在对应的 synchronized 语句中,无论是 wait() 方法或者 notify() 方法都需要首先获得目标对象的一个监视器。

下图显示了 wait() 方法和 notify() 方法的工作流程细节。

wait和notify工作流程

其中 T1 和 T2 表示两个线程。

T1 在正确执行 wait() 方法前,必须获得 object 对象的监视器。

而 wait() 方法在执行后,会释放这个监视器。

这样做的目的是使其他等待在 object 对象上的线程不至于因为 T1 的休眠而全部无法正常执行。

线程 T2 在 notify() 方法调用前,也必须获得 object 对象的监视器。

所幸,此时 T1 已经释放了这个监视器。因此,T2 可以顺利获得 object 对象的监视器。

接着,T2执行了 notify() 方法尝试唤醒一个等待线程,这里假设唤醒了 T1。

T1 在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得 object 对象的监视器,而这个监视器也正是 T1 在 wait() 方法执行前所持有的那个。

如果暂时无法获得,则 T1 还必须等待这个监视器。当监视器顺利获得后,T1 才可以在真正意义上继续执行。

源码(JDK 1.8.0_192)

Object#wait

/**
 * 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法。
 * 
 * 换句话说,这个方法的行为就像它只是执行调用 wait(0) 一样。 
 * 
 * 当前线程必须拥有此对象的监视器。
 * 
 * 线程释放此监视器的所有权并等待,直到另一个线程通过调用 notify 方法或 notifyAll 方法通知等待此对象监视器的线程唤醒。
 * 
 * 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。 
 * 
 * 在单参数版本中,中断和虚假唤醒是可能的,并且此方法应始终在循环中使用:
 *
 * <pre>
 *     synchronized (obj) {
 *         while (条件不满足的时候)
 *             obj.wait(timeout, nanos);
 *         ... // 执行适合条件的操作
 *     }
 * </pre>
 * 
 * 此方法只能由作为此对象监视器所有者的线程调用。
 * 
 * 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
 * 
 * @throws      IllegalMonitorStateException    如果当前线程不是此对象监视器的所有者。 
 * @throws      InterruptedException            如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
 */
public final void wait() throws InterruptedException {
    wait(0);
}

/**
 * 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法,或者经过指定的时间。 
 * 
 * 当前线程必须拥有此对象的监视器。 
 * 
 * 此方法导致当前线程(称为 T )将自身置于此对象的 wait set 中,然后放弃此对象上的所有同步声明。
 * 
 * 线程 T 因线程调度目的而被禁用,并处于休眠状态,直到发生以下四种情况之一: 
 * 
 * 1. 另一个线程调用这个对象的 notify 方法,而线程T恰好被任意选择为要唤醒的线程。 
 * 2. 其他一些线程为此对象调用 notifyAll 方法。 
 * 3. 其他线程会中断线程 T。 
 * 4. 指定的真实时间已过去,或多或少。但是,如果超时为 0,则不考虑实时性,线程只是等待通知。 
 * 
 * 然后从该对象的 wait set 中删除线程 T,并重新启用线程调度。
 * 
 * 然后,它以通常的方式与其他线程竞争在对象上同步的权利;
 * 
 * 一旦它获得了对对象的控制权,它对对象的所有同步声明都将恢复到原来的状态,也就是说,恢复到调用 wait 方法时的状态。
 * 
 * 然后,线程 T 从 wait 方法的调用返回。
 * 
 * 因此,从 wait 方法返回时,对象和线程 T 的同步状态与调用 wait 方法时的状态完全相同。 
 * 
 * 线程也可以在没有收到通知、中断或超时的情况下唤醒,这就是所谓的虚假唤醒。
 * 
 * 虽然这种情况在实践中很少发生,但应用程序必须通过测试本应导致线程被唤醒的条件来防范这种情况,如果条件不满足,则继续等待。
 * 
 * 换句话说,等待应该总是在循环中发生,比如:
 *
 * <pre>
 *     synchronized (obj) {
 *         while (条件不满足的时候)
 *             obj.wait(timeout, nanos);
 *         ... // 执行适合条件的操作
 *     }
 * </pre>
 * 
 * 如果当前线程在等待之前或等待期间被任何线程中断,则抛出 InterruptedException。
 * 
 * 在如上所述还原此对象的锁定状态之前,不会引发此异常。 
 * 
 * 注意,wait 方法在将当前线程放入这个对象的 wait set 中时,只解锁这个对象;当线程等待时,可以同步当前线程的任何其他对象将保持锁定状态。 
 * 
 * 此方法只能由作为此对象监视器所有者的线程调用。
 * 
 * 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
 *
 * @param       timeout                         以毫秒为单位的最长等待时间。 
 * @throws      IllegalArgumentException        如果超时值为负数或nanos的值不在0-999999范围内。 
 * @throws      IllegalMonitorStateException    如果当前线程不是此对象监视器的所有者。 
 * @throws      InterruptedException            如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
 */
public final native void wait(long timeout) throws InterruptedException;


/**
 * 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法,或者其他线程中断当前线程,或者经过一定的实际时间。 
 * 
 * 此方法类似于一个参数的 wait 方法,但它允许更好地控制在放弃之前等待通知的时间量。
 * 
 * 实时量(以纳秒为单位)由下式给出: 1000000 * timeout + nanos 
 * 
 * 在所有其他方面,此方法的作用与一个参数的方法 wait(long) 相同。
 * 
 * 特别地,wait(0,0) 与 wait(0) 的意思相同。 
 * 
 * 当前线程必须拥有此对象的监视器。
 * 
 * 线程释放此监视器的所有权,并等待发生以下两种情况之一: 
 * 
 * 1. 另一个线程通过调用 notify 方法或 notifyAll 方法通知等待此对象监视器的线程唤醒。
 * 2. 超时时间(由超时毫秒数加纳秒参数指定)已过。 
 * 
 * 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。 
 * 
 * 在单参数版本中,中断和虚假唤醒是可能的,并且此方法应始终在循环中使用: 
 *
 * <pre>
 *     synchronized (obj) {
 *         while (条件不满足的时候)
 *             obj.wait(timeout, nanos);
 *         ... // 执行适合条件的操作
 *     }
 * </pre>
 * 
 * 此方法只能由作为此对象监视器所有者的线程调用。
 * 
 * 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。 
 * 
 * @param       timeout                         以毫秒为单位的最长等待时间。 
 * @param       nanos                           附加时间,纳秒范围0-999999。 
 * @throws      IllegalArgumentException        如果超时值为负数或nanos的值不在0-999999范围内。 
 * @throws      IllegalMonitorStateException    如果当前线程不是此对象监视器的所有者。 
 * @throws      InterruptedException            如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
 */
public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
        "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

Object#notify


/**
 * 唤醒正在等待此对象监视器的单个线程。
 * 
 * 如果有任何线程正在等待这个对象,其中一个线程被选择唤醒。
 * 
 * 这种选择是任意的,由 实现类 自行决定。
 * 
 * 线程通过调用其中一个 wait 方法来等待对象的监视器。 
 * 
 * 在当前线程放弃对该对象的锁定之前,唤醒的线程将无法继续。
 * 
 * 唤醒的线程将以常规方式与任何其他线程竞争,这些线程可能会积极地在这个对象上进行同步竞争;例如,唤醒的线程在成为下一个锁定此对象的线程时并没有优劣可言。 
 * 
 * 此方法只能由作为此对象监视器所有者的线程调用。
 * 
 * 线程通过以下三种方式之一成为对象监视器的所有者: 
 * 
 * 1. 通过执行该对象的同步实例方法。 
 * 2. 通过执行在对象上同步的synchronized语句的主体。 
 * 3. 对于类类型的对象,执行该类的同步静态方法。 
 * 
 * 一次只能有一个线程拥有对象的监视器。
 *
 * @throws      IllegalMonitorStateException    如果当前线程不是此对象监视器的所有者。 
 */
public final native void notify();

/**
 * 唤醒等待此对象监视器的所有线程。
 * 
 * 线程通过调用其中一个 wait 方法来等待对象的监视器。 
 * 
 * 在当前线程放弃对该对象的锁定之前,唤醒的线程将无法继续。
 *
 * 唤醒的线程将以常规方式与任何其他线程竞争,这些线程可能会积极地在这个对象上进行同步竞争;例如,唤醒的线程在成为下一个锁定此对象的线程时并没有优劣可言。 
 * 
 * 此方法只能由作为此对象监视器所有者的线程调用。
 * 
 * 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
 *
 * @throws      IllegalMonitorStateException    如果当前线程不是此对象监视器的所有者。 
 */
public final native void notifyAll();
上一篇 下一篇