Java多线程系列—线程通信机制wait notify notifyAll(03)

1,499 阅读8分钟

线程通信机制wait notify notifyAll

本课时我们主要学习 wait/notify/notifyAll 方法的使用注意事项。

我们主要从三个问题入手:

  1. 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
  2. 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
  3. wait/notify 和 sleep 方法的异同?

wait 必须在 synchronized 保护的同步代码中使用

为什么wait 必须在 synchronized 保护的同步代码中使用,关于这一点我们可以看看wait 的源码注释,奈何有点长,我做一下删减

/**
 * Causes the current thread to wait until another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object.
 * In other words, this method behaves exactly as if it simply
 * performs the call {@code wait(0)}.
 * 使得当前线程进行等待,直到有其他线程对这个对象调用了notify或者notifyAll
 * 换句话说,这个方法的表现和调用wait(0)是一样的
 * <p>
 * The current thread must own this object's monitor. The thread
 * releases ownership of this monitor and waits until another thread
 * notifies threads waiting on this object's monitor to wake up
 * either through a call to the {@code notify} method or the
 * {@code notifyAll} method. The thread then waits until it can
 * re-obtain ownership of the monitor and resumes execution.
 * 当前线程必须是持有这个对象的monitor,调用wait 方法后,该线程将会释放monitor的所有权
 * 然后进入等待,直到有其他线程通过执行notify或者notifyAll唤醒在该monitor等待的线程
 * 被唤醒的线程开始的等待直到它可以获取monitor的所有权然后恢复执行
 * <p>
 * As in the one argument version, interrupts and spurious wakeups are
 * possible, and this method should always be used in a loop:
 * <pre>
 *     synchronized (obj) {
 *         while (&lt;condition does not hold&gt;)
 *             obj.wait();
 *         ... // Perform action appropriate to condition
 *     }
 * </pre>
 * 就像固定版本一样,打断和虚假唤醒也是可能的,所以这个方法总是在一个循环中使用
 * 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.
 * 这个方法应该被持有该对象monitor的线程调用,
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of the object's monitor.
 * 当前线程不是该对象monitor的持有这的时候抛出IllegalMonitorStateException
 * @throws  InterruptedException if any thread interrupted the
 *             current thread before or while the current thread
 *             was waiting for a notification.  The <i>interrupted
 *             status</i> of the current thread is cleared when
 *             this exception is thrown.
 *如果当前线程在等待的时候被任意线程打断,则抛出InterruptedException
 */
public final void wait() throws InterruptedException {
    wait(0);
}

其实到这里我们就知道为什么要在synchronized 里面使用了,因为当前线程首先要持有该对象的monitor 也就是锁,才能调用该对象的wait 方法,也就是说先拥有后使用。而且这个方法的调用形式一般都是

synchronized (obj) {
     while (&lt;condition does not hold&gt;)
         obj.wait();
     ... // Perform action appropriate to condition
}

我们是当满足什么样的条件的时候才去调用了wait进行等待换句话说就是不满足执行条件所以进行了等待,所以唤醒并且获得锁之后依然是需要判断是否要进入等待,因为唤醒它的时候不一定满足执行的条件,所以第一件事仍然是判断是否满足条件,我们将这种唤醒称之为虚假唤醒,这样即便被虚假唤醒了,也会再次检查while里面的条件,如果不满足条件,就会继续wait,也就消除了虚假唤醒的风险。

我们假设如果不需要synchronized,我们看看会发生什么,

public class WaitNotify {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
       buffer.add(data);
       // 通知可以消费了
       notify();
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            // 空的进行等待
            wait();
        }
        return buffer.remove();
    }
}

在代码中可以看到有两个方法,give 方法负责往 buffer 中添加数据,添加完之后执行 notify 方法来唤醒之前等待的线程,而 take 方法负责检查整个 buffer 是否为空,如果为空就进入等待,如果不为空就取出一个数据,这是典型的生产者消费者的思想。

但是这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:

  1. 首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
  2. 此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
  3. 此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。

虽然刚才消费者判断了 buffer.isEmpty 条件,但真正执行 wait 方法时,之前的 buffer.isEmpty 的结果已经过期了,不再符合最新的场景了,因为这里的“判断-执行”不是一个原子操作,它在中间被打断了,是线程不安全的。

假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 give 方法内的 notify 的唤醒,我们看到正是因为 wait 方法所在的 take 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作,那么此时整个程序就很容易出错。

其实到这里我们就知道wait为什么必须和synchronized配合使用。而且为什么使用的固定形式是while循环。

notify 必须在 synchronized 保护的同步代码中使用

其实和wait 的原因是一样的,因为只有这样才能保证是线程安全的,而关于notify和notifyAll的区别在于notify只唤醒等待该锁的一个线程,notifyAll幻想等待该锁的全部线程,但是只有一个线程能获得锁

notify的作用:

  1. 唤醒正在指定object wait的单个线程。 如果有多个线程在该对象上等待,则选择其中一个唤醒。
  2. 该选择是任意的,并且可以根据实现来决定。
  3. 被唤醒的线程将和任何需要获得指定object的monitor(锁)的线程竞争。
  4. 被唤醒的线程在作为获得指定object的monitor(锁)的下一个线程时没有任何优势。

为什么 wait/notify/notifyAll 被定义在 Object 类中而 sleep 定义在 Thread 类中

为什么 wait/notify/notifyAll 方法被定义在 Object 类中?而 sleep 方法定义在 Thread 类中?主要有两点原因:

  1. 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类,也就是说所有的对象都可以成为一个锁对象,所以这个时候你再回过头来看synchronized不应该叫锁的,它只是保证只有一个对象能在同一时刻获得锁的一种机制。
  2. 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。其实有点像是大家竞争一个公共的资源而不是线程本身就持有这个对象,这样就可能引发一些列的问题,例如监守自盗

wait 和 sleep 方法的异同

wait 和 sleep的异同

相似点

  1. 它们都可以让线程阻塞。
  2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点

  1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  2. 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
  3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  4. wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

总结

  1. notify不会释放锁,notify本身是在synchronized里的,synchronized块结束就会释放锁。

  2. 调用wait的线程被加入指定object的wait集合中,并且放弃在指定object上的同步声明,也就是我们说的释放锁

  3. 线程调用wait后,再次被唤醒的条件:

    1. 其他线程使用指定object调用notify,并且该线程碰巧被作为随机选择的线程去被唤醒
    2. 其他线程使用指定object调用notifyAll
    3. 其他线程interrupts该线程
    4. 指定的超时时间过去了(wait指定了超时时间,如locker.wait(1000)) 当超时时间为0时,超时时间被忽略,线程会一直休眠直到因为上面三种因素被唤醒
    5. 线程被虚假唤醒(线程可以在没有通知、中断或超时的情况下被唤醒,这就是所谓的假唤醒)