四、wait + notify + notifyAll

一、初识 wait、notify、notifyAll

我们知道由于线程的抢占式执行导致线程之间的调度是随机的,无序的。但是在一些场景下我们有需要合理的协调多个线程的执行顺序。我们知道使用 join 可以控制线程执行顺序,但是 join 只能让一个线程执行完在执行另外一个线程,功能有限。因此我们引入了 waitnotify/notifyAll 这样一组API用来更灵活地控制线程执行的顺序。

举个例子:

![file](https://pottercoding.cn/wp-content/uploads/2024/04/image-1712324623190.png)

针对上述情况,我们可以使用 wait/notify 有效解决:

假如张三发现ATM没钱,就(wait)释放锁走出ATM机,并进行阻塞等待(暂时不参与CPU调度,不参与锁竞争),直到有人向ATM机存钱后,此时时机成熟,就可以唤醒(notify)张三,继续参与ATM机锁竞争。

二、wait、notify、notifyAll 功能介绍

1、wait()

wait():发现条件不满足/时机不成熟,就调用 wait() 阻塞等待,同时释放当前锁,当满足一定条件时可以被唤醒,重新尝试获取这个锁。

:使用 wait() 阻塞等待会让线程进入WAITING状态

wait等待结束条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

2、notify()

notify():其他线程构造了一个成熟的条件,就可以调用 notify() 唤醒它。

notify注意事项:

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  2. 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  3. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。

3、notifyAll()

notifyAll():notify() 只是唤醒某一个等待线程。使用 notifyAll() 可以一次唤醒所有的等待同一对象的线程。

notifyAll注意事项:

虽然是同时唤醒 多个线程, 但是这 多个线程需要竞争锁,所以并不是同时执行,而仍然是先获取锁的先执行。

4、wait、notify、notifyAll 要点总结

  1. wait/notify/notifyAll 都是Object 的方法,因此只要是个类对象都可以使用 wait、notify、notifyAll。
  2. wait/notify/notifyAll 要搭配 synchronized 来使用. 脱离 synchronized 使用会直接抛出异常:IllegalMonitorStateException
  3. synchronized 的锁对象必须和调用 wait/notify/notifyAll 方法的对象是同一个。
  4. 针对某一加锁对象,先执行wait 然后 notify/notifyAll,此时才有效果,如果没有 wait 就 notify 相当于“一炮打空”,此时 wait 无法唤醒,不过代码不会出现其他异常。

5、wait/notify 使用示例

public class ThreadExample_wait_notify {

    public static void main(String[] args) throws InterruptedException {

        Object object = new Object();
        Thread t1 = new Thread(()->{

           synchronized (object) {

               try {

                   System.out.println("wait开始");
                   object.wait();
                   System.out.println("wait结束");
               } catch (InterruptedException e) {

                   e.printStackTrace();
               }
           }
        });

        Thread t2 = new Thread(()->{

           synchronized (object) {

               System.out.println("notify开始");
               object.notify();
               System.out.println("notify结束");
           }
        });
        // 等待t1中wait解锁阻塞,否则t2中的notify没有实质效果
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

![file](https://pottercoding.cn/wp-content/uploads/2024/04/image-1712324642838.png)

结果说明:

上述代码 t1 先获取到锁,执行到 wait 时 t1阻塞解锁,1秒后,t2获得锁,执行到 notify 时 t1 线程就会被唤醒,但是notify后不会立即释放锁,当 t2 代码块中的逻辑执行完后才释放锁,t1 线程才能获取锁继续向下执行。

三、wait、join、sleep 归纳

(1)wait
wait()方法是 Object类 提供的实例方法,可以使线程进入等待状态,直到其他线程调用了该对象的notify()或notifyAll()方法。通常被用于线程间通信,如生产者-消费者模式中(后续介绍),消费者需要等待生产者通知有新数据可取。当线程调用 wait() 方法时,它会释放占据的锁,并且线程的状态为WAITING,直到notify()或notifyAll()方法被调用。

(2)join
join() 方法是 Thread类 提供的方法静态方法,用于等待被调用线程执行完毕。在主线程中调用了子线程的join() 方法后,主线程会进入 WAITING 状态,直到子线程执行完毕才会继续执行。可以用来保证多个线程按照指定顺序执行。

(3)sleep
sleep()方法也是 Thread类 提供的实例方法,它可以使当前线程暂停执行一段时间。当线程调用 sleep() 方法时,它不会释放锁,线程的状态为 TIMED_WAITING 。通常被用于控制程序执行的速度或时间,或常常在循环内部以等待某些条件的发生。

总的来说,wait() 方法是用于线程间的通信,join() 方法是用于等待其他线程执行完毕,sleep()方法是用于暂停当前线程的执行。在使用上, wait 需要搭配 synchronized 使用,sleep 和 join 则不需要。总之本质上没有可比性,唯一的相同点就是都可以让线程放弃执行一段时间。它们的使用 场景 不同,需要根据实际需求选择合适的方法。