• 2023.10.17 关于 wait 和 notify 的使用


    目录

    引言

    方法的使用

    引入实例(wait 不带参数版本)

    wait 方法执行流程

    wait 和 notify 组合实例

    wait 带参数版本

    notify 和 notifyAll 的区别

    经典例题 

    总结 

    CountDownLatch


    引言

    • 线程最大的问题是抢占式执行,随机调度
    • 虽然线程在内核里的调度是随机的,但是可以通过一些api 来控制线程之间的执行顺序,让线程主动阻塞,主动放弃CPU,以便让给其他线程使用

    简单示例

    • 有线程t1 和 线程t2,希望线程t1 先干活,等到干的差不多了,再让线程t2 来干活
    • 此时就可以让线程t2 先 wait(阻塞,主动放弃CPU),等线程t1 干的差不多了,再通过 notify 通知线程t2,把线程t2 唤醒,让线程t2 接着干
    注意:

    join 或 sleep 与 wait 和 notify 之间使用的区别

    • 使用 join ,则表示线程t1 必须要彻底执行完,线程t2 才能运行,如果希望线程t1 先干 一半活,再让线程t2 接着干,此时 join 就不能满足需求
    • 使用 sleep 来指定一个休眠时间,同理我们难以知道线程t1 干一半活所需的具体时间,所以难以满足我们的需求
    • 可以认为 wait 和 notify 涵盖了 join 的用途,但是 wait 和 notify 的使用要比 join 麻烦很多,可以根据实际使用场景来自行选择

    方法的使用

    引入实例(wait 不带参数版本)

    1. public class ThreadDemo16 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object object = new Object();
    4. // wait 不加任何参数,就是死等,一直等待,直到有其他线程唤醒它
    5. object.wait();
    6. }
    7. }

    执行结果:

    • 该异常为非法的锁状态异常
    • 锁状态就两种一种是 加锁状态,一直是 解锁状态
    • 关于为啥会出现异常,我们还需了解 wait 方法的执行流程

    wait 方法执行流程

    • 先释放锁
    • 进行阻塞等待
    • 收到通知之后,重新尝试获取锁,并且在获取锁后,继续往下执行

    修改实例

    1. public class ThreadDemo16 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object object = new Object();
    4. // wait 不加任何参数,就是死等,一直等待,直到有其他线程唤醒它
    5. synchronized (object) {
    6. System.out.println("wait 之前");
    7. object.wait();
    8. System.out.println("wait 之后");
    9. }
    10. }
    11. }

    运行结果:

    • 相较于 object 未加锁就调用 wait 方法,而出现锁状态异常的报错
    • 这里先给 object 加上锁,再调用 wait 方法,就能很好的进行阻塞等待,并处于 WAITING 状态
    • 此处阻塞 释放掉了对象 object 的锁,从而其他线程 便可以获取对象 object 的锁

    wait 和 notify 组合实例

    1. package Thread;
    2. public class ThreadDemo17 {
    3. public static void main(String[] args) throws InterruptedException {
    4. Object object = new Object();
    5. Thread t1 = new Thread(() -> {
    6. // 这个线程负责进行等待
    7. System.out.println("t1: wait 之前");
    8. synchronized (object) {
    9. try {
    10. object.wait();
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. }
    15. System.out.println("t1: wait 之后");
    16. });
    17. Thread t2 = new Thread(() -> {
    18. // 这个线程负责唤醒
    19. System.out.println("t2: notify 之前");
    20. synchronized (object) {
    21. // notify 务必要获取到锁,才能进行通知
    22. object.notify();
    23. }
    24. System.out.println("t2: notify 之后");
    25. });
    26. t1.start();
    27. // 这里添加 sleep 等待1秒
    28. // 是为了能够尽量保证先执行线程t1 再执行线程t2
    29. Thread.sleep(1000);
    30. t2.start();
    31. }
    32. }

    运行结果:

    注意:

    • 此处的 notify 需和 wait 进行配对
    • 如果 wait 使用的对象和 notify 使用的对象不一致
    • 此时 notify 将不会有任何效果
    • 因为 notify 只能唤醒在同一个对象上等待的线程

    • 虽然这里的代码顺序是先执行线程t1 再执行线程t2
    • 但是由于线程调度的随机性,并不能完全保证一定是先执行线程t1 再执行线程t2 
    • 如果调用 notify 时没有线程在 wait,此时的 wait 是无法被唤醒的那么这种通知就是无效通知,但不会有啥副作用
    • 所以在执行线程t1 之后,先 sleep 等待1秒,再执行线程t2,能很大程度的保证线程t1 先执行 wait 方法

    wait 带参数版本

    • 上述代码的 wait 为无参数版本,意味着只要线程t2 不进行 notify,此时线程t1 就会始终 wait 下去,也就是死等
    • 所以 wait 带参数版本便能指定一个等待的最大时间, 能很好的避免死等情况的出现

    注意:

    • 虽然 wait 带参数版本,看起来跟 sleep 有点像
    • 都能指定等待时间
    • 都能被提前唤醒,wait 使用 notify ,sleep 使用 interrupt
    • 但是还是有本质差别的,其含义截然不同
    • notify 唤醒 wait,是正常的业务逻辑,并不会有任何异常
    • interrupt 唤醒 sleep 则会先触发中断异常,表示这是一个出了问题的逻辑

    notify 和 notifyAll 的区别

    • 当有多个线程等待 object 对象时
    • 有一个线程执行 notify 方法,那么将会随机唤醒一个等待的线程
    • 有一个线程执行 notifyAll 方法,那么将唤醒全部等待的线程,然后这些线程再一起竞争锁

    经典例题 

    • 有三个线程,分别只能打印 A、B、C ,控制三个线程固定按照 ABC 的顺序来打印

    具体思路

    • 我们可以创建两个对象 object1 和 object2
    • object1 用来控制线程t1和线程t2 的执行顺序
    • object2 用来控制线程t2和线程t3 的执行顺序
    • 让线程t3 wait 阻塞等待对象 object2,直到线程t2 执行 notify 
    • 让线程t2 wait 阻塞等待对象 object1,直到线程t1 执行 notify 
    • 这样便能很好的保证先执行线程t1 ,再执行线程t2,最后再执行线程t3
    1. public class ThreadDemo18 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object object1 = new Object();
    4. Object object2 = new Object();
    5. Thread t1 = new Thread(() -> {
    6. System.out.println("A");
    7. synchronized (object1) {
    8. object1.notify();
    9. }
    10. });
    11. Thread t2 = new Thread(() -> {
    12. synchronized (object1) {
    13. try {
    14. object1.wait();
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. System.out.println("B");
    20. synchronized (object2) {
    21. object2.notify();
    22. }
    23. });
    24. Thread t3 = new Thread(() -> {
    25. synchronized (object2) {
    26. try {
    27. object2.wait();
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. }
    31. }
    32. System.out.println("C");
    33. });
    34. t2.start();
    35. t3.start();
    36. Thread.sleep(500);
    37. t1.start();
    38. }
    39. }

    运行结果:

    注意:

    • 之所以这样安排线程的执行顺序并在执行线程t1 前先等待 0.5秒
    • 是因为能很大程度上避免线程t1 执行 notify 之后,线程t2 还未执行 wait 方法阻塞等待对象 object1 
    • 从而导致线程t1 notify 了个寂寞,便会导致线程t2 一直阻塞等待,出现死锁的情况

    总结 

    • wait 和 notify 这两个 api 是用来控制线程之间执行顺序的
    • wait 和 notify 均属于 Object 类的方法

    CountDownLatch

    类比理解

    • 此处有一个跑步比赛

    • 这场跑步比赛,开始时间是明确的(裁判开发令枪即开始)
    • 结束时间,则是不明确的(所有选手均冲过终点线即结束)
    • 为了等待这个跑步比赛结束,便引入这个 CountDownLatch

    两个方法

    • await(wait是等待,a 代表 all),主线程来调用该方法
    • countDown 表示选手冲过了终点线

    具体思路

    • CountDownLatch 在构造的时候,指定一个计数(此处为选手的个数)
    • 例如,四个选手进行比赛
    • 初始情况下调用 await,就会阻塞
    • 每个选手都冲过终点,都会调用 countDown 方法
    • 前三次调用 countDown 方法,await 没有任何影响
    • 第四次调用 countDown 方法,await 就会被唤醒,返回(解除阻塞)
    • 此时就可以认为是整个比赛都结束了

    应用场景

    • 下载一个大文件,例如 迅雷、steam 等进行多线程下载
    • 多线程下载,即把一个大的文件,切分成多个小块的文件,安排多个线程分别下载
    • 此处就可以使用 CountDownLatch 来进行区分是不是整体下载完了
  • 相关阅读:
    Nginx学习总结
    webrtc QOS笔记一 Neteq直方图算法浅读
    asp.net+sqlserver笔记本电脑售后服务管理系统C#
    npm学习:安装、更新以及管理npm版本
    内存池autobuffer
    尚硅谷–MySQL–基础篇(P1~P95)
    C++ String类的简单实现(非模板)
    优化elementUI的Message消息提示连续触发,满屏显示问题
    现有TiDB集群扩展pump/drainer作为binlog文件落地
    Lagrange Multipliers 拉格朗日乘数法(含 KKT 条件)
  • 原文地址:https://blog.csdn.net/weixin_63888301/article/details/133880746