【JavaEE初阶 — 多线程】wait() & notify()

1. 协调多个线程之间的执行先后顺序的方法介绍


由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知;但是实际开发中,有时候我们希望合理地协调多个线程之间的执行先后顺序。

拓展: wait() 和 sleep() 的区别


wait() 和 sleep()都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):


(1)使用要求不同


  • wait() 必须在同步块或同步方法内调用(嵌套一层 synchronized ),否则会抛出IllegalMonitorStateException。
  • 这是因为 wait() 依赖于对象锁来管理线程的等待和唤醒机制。
  • 调用后,当前线程会释放它持有的对象锁,并进入等待状态。

  • sleep()方法可以在任何上下文中调用,不需要获取对象锁。
  • 调用后,线程会进入休眠状态,但不会释放它持有的任何锁。
  • 所以如果 wait() 和 sleep() 都嵌套一层锁,分别被唤醒时,wait() 会释放锁,而 sleep() 不会释放锁;

(2)方法所属类不同


  • wait() :属于 Object 类的非静态方法。
  • sleep() :属于 Thread 类的静态方法。

(3)恢复方式不同


  • wait() 需要被其他线程通过 notify() 或 notifyAll() 显式唤醒;
  • 或者被 wait(long timeout) 的超时参数唤醒。

  • sleep() 在指定时间后自动恢复运行,或通过 interrupt() 提前唤醒,抛出 InterruptedException 异常。

(4)用途不同


  • wait() 通常用于线程间通信,配合 notify() 或 notifyAll() 来实现线程的协调工作。

  • sleep() 用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。

(5)常见错误


误用sleep() :

有时开发者会错误地使用 sleep() 进行线程间通信,但是 sleep() 不释放锁,可能会导致其他线程无法进入同步块,造成线程饥饿或死锁。


忽略中断:

sleep() 可能抛出 InterruptedException , 如果不正确处理中断信号,可能会导致线程提前退出或错误行为。


2. wait()


2.1 线程饿死

如上图,鸟妈妈(CPU)抓虫(调度资源)喂小鸟(线程),就是一个典型的 "线程饿死" 情景:

  • 对于线程饿死,并不是鸟妈妈把捉到的虫全都喂一只鸟宝宝,其他鸟宝宝一点都吃不到,而是鸟妈妈把捉到的虫子,绝大多数都喂给了一个鸟宝宝,剩下的鸟宝宝只能吃到一点点;
  • 使用 wait(),notify() 就是为了优化 "鸟妈妈把大多数的虫子,都分给一只鸟宝宝" 这一行为。

2.2 调用 wait()


  • wait() 和 notify 都是Object 的方法;Java 中的任意对象都提供了 wait() 和 notify();
  • wait() 能使当前执行代码的线程进行等待(把线程放到等待队列中);
  • wait() 一被调用,就会释放当前的锁;
  • 满足一定条件时被唤醒,重新尝试获取这个锁;
  • 注意:在判断是否满足唤醒条件时,我们可以把 if(判断条件) 改成 while(判断条件) ,这样可以避免被 interrupt() 类似的方法非法打断。在该文章模拟阻塞队列的 put() 和 take() 有详细解释

  • wait 要搭配 synchronized 来使用;脱离synchronized,使用 wait 会直接抛出上述异常;
  • 上述异常被抛出的本质,是针对未加锁的锁对象进行释放锁操作;

2.3 唤醒 wait()

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

  • 在synchronized的代码块中,等到wait() 结束,wait后面到 }的部分,还有一些其他的逻辑;
  • 这些逻辑还是期望在锁的范围内进行调度,所以 wait 后面到}的部分也要嵌套在锁内,wait() 被唤醒后,会重新对这些逻辑上锁,以保证线程安全。

3. notify()


3.1 wait() 和 notify() 的需要同一个对象调用


  • 通过相同的对象调用 wait() 和 notify() ,是两个线程沟通的桥梁;
  • wait() 和 notify() 针对同一个对象才能生效;如果是不同对象,则没有任何相互的影响和作用~
  • 为了验证这一点,外面写出如下代码:

代码逻辑:

  • 在 t1 线程执行到第一个打印日志之后,执行 wait() ,此时就需要通过 t2 线程来唤醒 t1;
  • 我们先用 Scaner 来阻塞 t2,这样操作就可以手动控制 t2 对 t1 的唤醒;
  • 在输入内容后,t2执行 notify(),如果调用 wait() 和 notify() 的两个对象相同,则 t1 会成功被唤醒。

上图是不同对象调用 wait() 和 notify() 的情况,我们再来看看相同对象调用的结果:


3.2 notify() 要 同步方法或同步块中调用


和 wait() 一样,也是要在同步方法或同步块(嵌套一层 synchronized)中调用;


3.3 notify() 随机唤醒多个 wait() 中的其中一个


  • notify()通知正在 wait(), 等待同一个对象锁的其它线程,对其它线程中的一个线程,发出通知notify,并使这个线程重新获取该对象的对象锁;
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程。(并没有"先来后到");
  • 我们来看程序运行结果,在输入任意内容后,打印的结束日志是不一样的;

  • 这就证明了线程调度器随机挑选出一个呈 wait 状态的线程;
  • 有两个 wait(),一个 notify(),一定有一个线程没有被唤醒,导致整个进行无法结束;
  • 既然有两个 wait(),我们就设置两个 notify() 即可解决该问题;

  • 在 notify()方法后,当前线程不会马上释放该对象锁;
  • 要等到执行 notify() 所在同步代码块退出之后,才会释放对象锁 ;

4. notifyAll()


  • 对于刚刚上面写的代码,只有一个 notify(),就只能唤醒一个 wait():

  • 因此,我们可以考虑用 notifyAll(),唤醒所有相同对象调用 wait() 的线程:

  • 虽然同时唤醒了 t1 和 t2,但是由于 wait() 被唤醒之后,要重新加锁;因此其中某个线程,先加上锁,开始执行,而另一个线程因为加锁失败,再次阻塞等待;
  • 等到先加锁的线程解锁了,后加锁的线程才能加上锁,而继续执行~

总结

  • 因为这个原因,notifyAll() 在实际开发中,虽然可以唤醒所有 wait() ,但是用的并不多。
  • 因为不是一口气全部唤醒 wait(),而是每次唤醒其中一个线程,通过多次唤醒,把所有 wait()状态的线程唤醒;
  • notifyAll() 在唤醒其中一个 wait() 状态的线程时,其他线程依旧因为 wait() 尝试重新获取锁对象,而陷入阻塞等待;
  • 比起唤醒所有,我们更希望通过一个一个的 notify() 精确唤醒每一个线程。

5. 应用 wait() 和 notify() 解决编程题


5.1 题目


5.2 程序运行结果


5.3 完整代码

java 复制代码
package Thread;

public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker1) {
                        locker1.wait();
                    }
                    System.out.print("A");
                    synchronized (locker2){
                        locker2.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker2) {
                        locker2.wait();
                    }
                    System.out.print("B");
                    synchronized (locker3){
                        locker3.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t3 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker3) {
                        locker3.wait();
                    }
                    System.out.println("C");
                    synchronized (locker1){
                        locker1.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
        t3.start();
        
        //轮子已经造好了,现在需要推一把,让轮子转起来
        //需要确保上述主线程都执行到 wait(),再推轮子
        Thread.sleep(1000);
        synchronized (locker1){
            locker1.notify();
        }
    }
}

相关推荐
诚丞成3 分钟前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
Smile灬凉城66615 分钟前
反序列化为啥可以利用加号绕过php正则匹配
开发语言·php
lsx20240627 分钟前
SQL MID()
开发语言
Dream_Snowar30 分钟前
速通Python 第四节——函数
开发语言·python·算法
西猫雷婶31 分钟前
python学opencv|读取图像(十四)BGR图像和HSV图像通道拆分
开发语言·python·opencv
鸿蒙自习室32 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
星河梦瑾32 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富35 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想37 分钟前
JMeter 使用详解
java·jmeter
言、雲39 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库