线程间的同步与通信(2)——wait, notify, notifyAll

前言

上一篇文章我们讲了java的同步代码块, 这一篇我们来看看同步代码块之间的协作与通信.

阅读本篇前你需要知道什么是同步代码块, 什么是监视器锁, 还不是很了解的同学建议先去看一看上一篇文章.

本文的源码基于JDK1.8

概述

在Java中, 我们可以使用

  • wait()
  • wait(long timeout)
  • wait(long timeout, int nanos)
  • notify()
  • notifyAll()

这5个方法来实现同步代码块之间的通信, 注意, 我说的是同步代码块之间的通信, 这意味着:

调用该方法的当前线程必须持有对象的监视器锁

(源码注释: The current thread must own this object’s monitor.)

其实, 这句话换个通俗点的说法就是: 只能在同步代码块中使用这些方法.

道理很简单, 因为只有进入了同步代块, 才能获得监视器锁.

wait方法的作用是, 阻塞当前线程(阻塞的原因常常是一些必要的条件还没有满足), 让出监视器锁, 不再参与锁竞争, 直到其他线程来通知(告知必要的条件已经满足了), 或者直到设定的超时等待时间到了.

notifynotifyAll方法的作用是, 通知那些调用了wait方法的线程, 让它们从wait处返回.

可见, waitnotify 方法一般是成对使用的, 我把它简单的总结为:

等通知

wait 是等, notify 是通知.

为了给大家一个感性的认识, 我这里打个比方:

假设你和舍友一起租了个两室一厅一厨一卫的房子, 天这么热, 当然每天都要洗澡啦, 但是卫生间只有一个, 同一时间, 只有一个人能用.

这时候, 你先下班回来了, 准备要洗澡, 刚进浴室, 突然想起来你的专用防脱洗发膏用完了, 查了下快递说是1小时后才能送到, 但这时候你的舍友回来了, 他也要洗澡, 所以你总不能”站着茅坑不拉屎”吧, 所以你主动让出了浴室(调用wait方法, 让出监视器锁), 让舍友先洗, 自己快递.

过了一个小时, 快递送来了你的防脱洗发膏(调用了nofity方法, 唤醒在wait中的线程), 你现在需要洗澡的资源都有了, 万事俱备, 就差进入浴室了, 这个时候你去浴室门口一看, 嘿, 浴室空着!(当前没有线程占用监视器锁) 舍友已经洗好了! 于是你高高兴兴的带着你的防脱洗发水进去洗澡了(再次获得监视器锁).

当然, 上面还有另外一种情况, 假如你不知道快递员什么时候会来, 可能在一小时后, 也可能是明天, 那总不能一直干等着不洗澡吧, 于是你决定, 我就等一个小时(调用带超时时间的wait(long timeout)方法), 一小时后快递还不来, 就不等了, 大不了用沐浴露凑合着洗洗头 o(TヘTo)

上面只是拿生活中的例子打了个比方, 不知道大家理解了没有, 下面我们就来正经的看看代码.

源码分析

以上5个都方法定义在了java的Object类中, 这意味着java中所有的类都会继承这些方法.
同时, 下面的源码分析中我们将看到, 这些方法都是final类型的, 也就是说所有的子类都不能改写这些方法.

下面我们来看源码:

(这一段会比较长, 不想看源码分析的可以直接跳过这一部分看结论)

wait方法

public final void wait() throws InterruptedException {
    wait(0);
}

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);
}

public final native void wait(long timeout) throws InterruptedException;

wait方法共有三个, 我们发现, 前两个方法都是调用了最后一个方法, 而最后一个方法是一个native方法.

我们知道, native方法是非java代码实现的, 我们看不到它的具体实现内容, 但是java规定了该方法要实现什么样的功能, 即它应该在java代码里”看起来是什么样子的”.
所以native方法就像java的接口一样, 但是具体实现由JVM直接提供,或者(更多情况下)由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

在Object的源码的注释中, 描述了该native方法”看起来应该是什么样子的”, 我们一段一段来看:

(这里我把原文也贴出来了, 是怕自己翻译的不够精确, 英语好的可以直接看原文)

/**
 * Causes the current thread to wait until either another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object, or a
 * specified amount of time has elapsed.
 * <p>
 * The current thread must own this object's monitor.
 * <p>
 ...
 */

这段是说, 该方法导致了当前线程挂起, 直到其他线程调用了这个objectnotify或者notifyAll方法, 或者设置的超时时间到了(超时时间即timeout参数的值, 以毫秒为单位), 另外它提到了, 当前线程必须已经拿到了监视器锁, 这点我们在开篇的概论中已经提到了.

/* 
 ...
 * This method causes the current thread (call it <var>T</var>) to
 * place itself in the wait set for this object and then to relinquish
 * any and all synchronization claims on this object. Thread <var>T</var>
 * becomes disabled for thread scheduling purposes and lies dormant
 * until one of four things happens:
 * <ul>
 * <li>Some other thread invokes the {@code notify} method for this
 * object and thread <var>T</var> happens to be arbitrarily chosen as
 * the thread to be awakened.
 * <li>Some other thread invokes the {@code notifyAll} method for this
 * object.
 * <li>Some other thread {@linkplain Thread#interrupt() interrupts}
 * thread <var>T</var>.
 * <li>The specified amount of real time has elapsed, more or less.  If
 * {@code timeout} is zero, however, then real time is not taken into
 * consideration and the thread simply waits until notified.
 * </ul>
 * The thread <var>T</var> is then removed from the wait set for this
 * object and re-enabled for thread scheduling. It then competes in the
 * usual manner with other threads for the right to synchronize on the
 * object; once it has gained control of the object, all its
 * synchronization claims on the object are restored to the status quo
 * ante - that is, to the situation as of the time that the {@code wait}
 * method was invoked. Thread <var>T</var> then returns from the
 * invocation of the {@code wait} method. Thus, on return from the
 * {@code wait} method, the synchronization state of the object and of
 * thread {@code T} is exactly as it was when the {@code wait} method
 * was invoked.
 ...
*/

这段话的大意是说, 该方法使得当前线程进入当前监视器锁(this object)的等待队列中(wait set), 并且放弃一切已经拥有的(这个监视器锁上)的同步资源, 然后挂起当前线程, 直到以下四个条件之一发生:

  1. 其他线程调用了this objectnotify方法, 并且当前线程恰好是被选中来唤醒的那一个(下面分析notify的时候我们就会知道, 该方法会随机选择一个线程去唤醒)
  2. 其他线程调用了this objectnotifyAll方法,
  3. 其他线程中断了(interrupt)了当前线程
  4. 指定的超时时间到了.(如果指定的时间是0, 则该线程会一直等待, 直到收到其他线程的通知)

这里插一句, 关于第四条, 解释了无参的wait方法:

public final void wait() throws InterruptedException {
    wait(0);
}

我们知道, 无参的wait方法的超时时间就是0, 也就是说他会无限期等待, 直到其他线程调用了notify
或者notifyAll.

同时, 我们再看另一个有两个参数的wait方法:

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);
}

这个方法在其源码的注释中号称是实现了纳秒级别的更精细的控制:

/* 
 *This method is similar to the {@code wait} method of one
 * argument, but it allows finer control over the amount of time to
 * wait for a notification before giving up. The amount of real time,
 * measured in nanoseconds, is given by:
 * <blockquote>
 * <pre>
 * 1000000*timeout+nanos</pre></blockquote>
 * <p>
 * In all other respects, this method does the same thing as the
 * method {@link #wait(long)} of one argument. In particular,
 * {@code wait(0, 0)} means the same thing as {@code wait(0)}.
 * <p>
 ...
 */

但是我们实际看源码可知, 当nanos的值大于0但低于999999时, 即低于1毫秒时, 就直接将timeout++了, 所以这里哪里来的纳秒级别的控制??? 最后不还是以毫秒为粒度吗? 不过是多加一毫秒而已. 这个方法真的不是在卖萌吗?(  ̄ー ̄)

注意, 这里同样说明了 wait(0,0)wait(0)是等效的, 这点其实直接将值代入源码也能得出这个结论.

好了, 吐槽完毕, 我们接着看剩下来的注释:

/*
 ...
 * The thread <var>T</var> is then removed from the wait set for this
 * object and re-enabled for thread scheduling. It then competes in the
 * usual manner with other threads for the right to synchronize on the
 * object; once it has gained control of the object, all its
 * synchronization claims on the object are restored to the status quo
 * ante - that is, to the situation as of the time that the {@code wait}
 * method was invoked. Thread <var>T</var> then returns from the
 * invocation of the {@code wait} method. Thus, on return from the
 * {@code wait} method, the synchronization state of the object and of
 * thread {@code T} is exactly as it was when the {@code wait} method
 * was invoked.
 ...
 */

这一段说的就是满足了上面四个条件之一之后的事情了, 此时该线程会从wait set中移除, 重新参与到线程调度中, 并且和其他线程一样, 竞争锁资源, 一旦它又获得了监视器锁, 则它在调用wait方法时的所有状态都会被恢复, 即我们熟知的恢复现场.

/*
 ...
 * <p>
 * A thread can also wake up without being notified, interrupted, or
 * timing out, a so-called <i>spurious wakeup</i>.  While this will rarely
 * occur in practice, applications must guard against it by testing for
 * the condition that should have caused the thread to be awakened, and
 * continuing to wait if the condition is not satisfied.  In other words,
 * waits should always occur in loops, like this one:
 * <pre>
 *     synchronized (obj) {
 *         while (&lt;condition does not hold&gt;)
 *             obj.wait(timeout);
 *         ... // Perform action appropriate to condition
 *     }
 * </pre>
 * (For more information on this topic, see Section 3.2.3 in Doug Lea's
 * "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
 * 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
 * Language Guide" (Addison-Wesley, 2001).
 ...
 */

这一段是说即使没有满足上面4个条件之一, 线程也可能被唤醒, 称之为假唤醒, 虽然这种情况很少出现, 但是作者建议我们将wait放在循环体中, 并且检测唤醒条件是不是真的满足了, 并且还:
推荐了两本书…
推荐了两本书…
推荐了两本书…
还愣着干嘛, 赶紧去买书呀(~ ̄(OO) ̄)ブ

/*
 ...
 * <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
 * interrupted} by any thread before or while it is waiting, then an
 * {@code InterruptedException} is thrown.  This exception is not
 * thrown until the lock status of this object has been restored as
 * described above.
 ...
 */

这段解释了中断部分, 说的是当前线程在进入wait set之前或者在wait set之中时, 如果被其他线程中断了, 则会抛出InterruptedException异常, 但是, 如果是在恢复现场的过程中被中断了, 则直到现场恢复完成后才会抛出InterruptedException(这段不知道我理解的对不对, 因为对This exception is not thrown until the lock status of this object has been restored as described above.的翻译不是很确信)

/*
 ...
 * <p>
 * Note that the {@code wait} method, as it places the current thread
 * into the wait set for this object, unlocks only this object; any
 * other objects on which the current thread may be synchronized remain
 * locked while the thread waits.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 */

这段话的意思是说, 即使wait方法把当前线程放入this objectwait set里, 也只会释放当前监视器锁(this object), 如果当前线程还持有了其他同步资源, 则即使当前线程被挂起了, 也不会释放这些资源.
同时, 这里也提到, 该方法只能被已经持有了监视器锁的线程所调用.

到这里, wait方法我们就分析完了, 虽然它是一个native方法, 源码中并没有具体实现, 但是java规定了该方法的行为, 这些都体现了源码的注释中了.
同时, 我们的分析中多次出现了 monitor, this object, wait set等术语, 这些概念涉及到wait方法的实现细节, 我们后面会讲.

notify & notifyAll

notify和notifyAll方法都是native方法:

public final native void notify();
public final native void notifyAll();

相比于wait方法, 这两个方法的源码注释要少一点, 我们就不分段看了, 直接看全部的

notify

/**
 * Wakes up a single thread that is waiting on this object's
 * monitor. If any threads are waiting on this object, one of them
 * is chosen to be awakened. The choice is arbitrary and occurs at
 * the discretion of the implementation. A thread waits on an object's
 * monitor by calling one of the {@code wait} methods.
 * <p>
 * The awakened thread will not be able to proceed until the current
 * thread relinquishes the lock on this object. The awakened thread will
 * compete in the usual manner with any other threads that might be
 * actively competing to synchronize on this object; for example, the
 * awakened thread enjoys no reliable privilege or disadvantage in being
 * the next thread to lock this object.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. A thread becomes the owner of the
 * object's monitor in one of three ways:
 * <ul>
 * <li>By executing a synchronized instance method of that object.
 * <li>By executing the body of a {@code synchronized} statement
 *     that synchronizes on the object.
 * <li>For objects of type {@code Class,} by executing a
 *     synchronized static method of that class.
 * </ul>
 * <p>
 * Only one thread at a time can own an object's monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of this object's monitor.
 * @see        java.lang.Object#notifyAll()
 * @see        java.lang.Object#wait()
 */

上面这段是说:

  • notify方法会在所有等待监视器锁的线程中任意选一个唤醒, 具体唤醒哪一个, 交由该方法的实现者自己决定.
  • 被唤醒的线程只有等到当前持有锁的线程完全释放了锁才能继续.(这里解释下, 因为调用notify方法时, 线程还在同步代码块里面, 只有离开了同步代码块, 锁才会被释放)
  • 被唤醒的线程和其他所有竞争这个监视器锁的线程地位是一样的, 既不享有优先权, 也不占劣势.
  • 这个方法应当只被持有监视器锁的线程调用, 一个线程可以通过以下三种方法之一获得this object的监视器锁:

    • 通过执行该对象的普通同步方法
    • 通过执行synchonized代码块, 该代码块以this object作为锁
    • 通过执行该类的静态同步方法

我们通过上一篇介绍synchronized同步代码块的文章知道, synchronized作用于类的静态方法时, 是拿类的Class对象作为锁, 作用于类的普通方法或者 synchronized(this){}代码块时, 是拿当前类的实例对象作为监视器锁, 这里的this object, 指的应该是该线程调用notify方法所持有的锁对象.

notifyAll

/**
 * Wakes up all threads that are waiting on this object's monitor. A
 * thread waits on an object's monitor by calling one of the
 * {@code wait} methods.
 * <p>
 * The awakened threads will not be able to proceed until the current
 * thread relinquishes the lock on this object. The awakened threads
 * will compete in the usual manner with any other threads that might
 * be actively competing to synchronize on this object; for example,
 * the awakened threads enjoy no reliable privilege or disadvantage in
 * being the next thread to lock this object.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of this object's monitor.
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#wait()
 */

上面这段是说: notifyAll方法会唤醒所有等待this object监视器锁的线程, 其他内容和notify一致.

总结

总则: 调用这5个方法的线程必须持有监视器锁。

  1. wait方法会使当前线程进入自己所持有的监视器锁(this object)的等待队列中, 并且放弃一切已经拥有的(这个监视器锁上的)同步资源, 然后挂起当前线程, 直到以下四个条件之一发生:

    1. 其他线程调用了this objectnotify方法, 并且当前线程恰好是被选中来唤醒的那一个
    2. 其他线程调用了this objectnotifyAll方法,
    3. 其他线程中断了当前线程
    4. 指定的超时时间到了.(如果指定的超时时间是0, 则该线程会一直等待, 直到收到其他线程的通知)
  2. 当以上4个条件之一满足后, 该线程从wait set中移除, 重新参与到线程调度中, 并且和其他线程一样, 竞争锁资源, 一旦它又获得了监视器锁, 则它在调用wait方法时的所有状态都会被恢复, 这里要注意“假唤醒”的问题.
  3. 当前线程在进入wait set之前或者在wait set之中时, 如果被其他线程中断了, 则会抛出InterruptedException异常, 但是, 如果是在恢复现场的过程中被中断了, 则直到现场恢复完成后才会抛出InterruptedException
  4. 即使wait方法把当前线程放入this objectwait set里, 也只会释放当前监视器锁(this object), 如果当前线程还持有了其他同步资源, 则即使它在this object中的等待队列中, 也不会释放.
  5. notify方法会在所有等待监视器锁的线程中任意选一个唤醒, 具体唤醒哪一个, 交由该方法的实现者自己决定.
  6. 线程调用notify方法后不会立即释放监视器锁,只有退出同步代码块后,才会释放锁(与之相对,调用wait方法会立即释放监视器锁)
  7. 线程被notify或notifyAll唤醒后会继续和其他普通线程一样竞争锁资源

思考题

本篇中多次提到了monitor, this object, wait set等概念,这些都代表什么意思?

监视器锁到底是怎么获取和释放的?

我们将在下一篇文章讨论这个问题。

(完)

    原文作者:Java多线程
    原文地址: https://segmentfault.com/a/1190000016002355
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞