当前位置: 首页 > article >正文

线程的等待与通知

我们知道. 线程在操作系统上是随机调度执行的. 那么如果我们想要控制线程之间某个逻辑执行的先后顺序, 该怎么办呢?  -->  wait和notify能够为我们解决这个问题.

在Java中, wait()方法是Object类提供的一个方法, 当一个线程调用了某个对象的wait()方法时, 该线程会暂停执行,并等待直到另一个线程在该对象上调用notify()或notifyAll()方法, 这通常用于实现线程间的通信.

我们可以让后执行的线程使用wait进行等待, 当先执行的线程执行完某些逻辑之后, 通过notify唤醒对应的wait, 相当于"通知"了后执行的线程, 让后执行的线程继续执行. 

[注]: wait中一定会执行"解锁"的操作, 所以在调用wait()方法时, 一定要将其放到synchronized代码块中 (先加上锁才能谈解锁), 否则程序就会报错. 另外, notify()的执行也需要持有锁资源 (放到synchronized里面), 否则程序也会报错.

一. 单个线程的等待和唤醒

1. 例子

import java.util.Scanner;

public class Demo21 {
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 wait 之前");
                try {
                    locker.wait(); // 释放锁并进入阻塞状态.
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2 notify 之前");
            Scanner scanner = new Scanner(System.in);
            scanner.next(); // 此处用户输入啥都行, 主要是通过这个 next, 构造一个"阻塞"状态.

            synchronized (locker) { //线程t1执行wait就会将locker释放, 所以这里线程t2可以顺利拿到locker
                locker.notify();
            }

            System.out.println("t2 notify 之后");
        });

        t1.start();
        t2.start();
    }
}

 线程t1是后执行的线程, 执行wait()进入等待状态. 线程t2是先执行的线程, 执行完它的任务之后会执行notify(), 通知线程t1结束等待. 我们看运行结果:

我们可以看到, 当线程t2执行notify通知t1结束线程后, t1立刻结束等待, 执行后面的语句"t1 wait 之后". 

[注意]: wait执行之后, 是要执行3部分内容的: (1) 释放锁资源. (2) 进入等待状态. (3) 收到notify通知之后, 唤醒等待状态, 继续往下执行. 由于这里执行wait会释放锁资源, 所以无法保证wait中执行的指令是"原子的", 所以就有可能出现一种情况: wait释放锁资源之后, 话没有来得及开始等待, 另一个线程就执行notify了. 由于当前线程还没有进入真正的等待状态. 所以它无法接收到notify的通知. 所以也就无法结束等待, 会持续阻塞下去.

wait还有带时间参数的版本: wait(long timeout), 这个版本指定了超时时间, 如果已经达到了最大等待时间还没有接收到notify通知的话, 那么wait会自己结束等待状态, 继续往下执行.

2. wait(long timeout)和sleep(long timeout)的区别

(1) 使用wait的目的是为了唤醒, 而sleep就是固定时间的阻塞, 不涉及唤醒.

(2) wait必须要搭配synchronized使用, 而sleep与加不加锁没有任何关系.

二. 多个线程的等待和唤醒

1. 例子

import java.util.Scanner;

public class Demo22 {
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 wait 之后");
            }
        });
        Thread t3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t3 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t3 wait 之后");
            }
        });

        Thread t4 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            scanner.next(); //阻塞状态
            synchronized (locker) {
                locker.notify();
                locker.notify();
                locker.notify();
            }
            System.out.println("t4 notify 之后");
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

上述代码三个线程执行三个wait(), t4线程执行三个notify来唤醒这三个线程.

有三个wait应该notify三次, 但是notify多次也可以 (没有任何副作用). 

2. notifyAll()

上面是三个线程等待, 唤醒三次, 那如果有很多个线程等待呢? --> java中提供了一个notifyAll()方法, 用于一次性唤醒全部线程.  

3. 规定执行顺序

public class Demo23 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
                System.out.print("A");
                try {
                    Thread.sleep(100); //这里休眠100ms保证是t2先拿到locker1进入wait状态的.
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    locker1.notify();
                }
        });
        Thread t2 = new Thread(() -> {
                synchronized (locker1) {
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                System.out.print("B");

                synchronized (locker2) {
                    locker2.notify();
                }
            }
        });
        Thread t3 = new Thread(() -> {
                synchronized (locker2) {
                    try {
                        locker2.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                System.out.println("C");
            }
        });

        t1.start();
        t2.start();
        t3.start();
        }
    }

如上述代码: 创建了两个锁对象locker1和locker2. 其中t2线程拿到locker1并释放进入等待, t3线程拿到locker2并释放进入等待. t1线程打印完"A"之后, 拿到locker1通知线程t2结束等待. t2结束等待之后, 打印"B", 然后拿到locker2, 通知线程t3结束等待. 然后t3打印"C". 所以这个代码的执行结果只能是ABC. (但是如果不加wait和notify的话, 实际的执行情况并不能确定).

 

三. 小结

(1) wait和notify的主要作用是对线程的执行顺序做出干预

(2) wait和notify都是Object类提供的方法.

(2) wait和notify都需要搭配synchronized来使用.

(3) wait方法提供了无参版本, 也提供了带超时时间参数的版本.

(4) 如果有多个线程都处于wait状态, 但是只有一个notify, 那么这个notify会随机唤醒其中一个wait状态的线程.

(5) notifyAll能够唤醒所有正在wait的线程.

(6) 如果notify调用次数超过了wait的次数, 会将全部wait都唤醒. (没有任何副作用).

 好了, 本篇文章就介绍到这里啦, 大家如果有疑问欢迎评论, 如果喜欢小编的文章, 记得点赞收藏~~

 


http://www.kler.cn/a/380196.html

相关文章:

  • 在K8s平台部署个人博客
  • HarmonyOS 移动应用开发
  • 如何将钉钉新收款单数据高效集成到MySQL
  • 【初阶数据结构篇】链式结构二叉树(二叉链)的实现(感受递归暴力美学)
  • 利用EasyExcel实现简易Excel导出
  • 基于vue3和elementPlus的el-tree组件,实现树结构穿梭框,支持数据回显和懒加载
  • 谷歌浏览器怎么设置网页自动刷新
  • OpenGL入门006——着色器在纹理混合中的应用
  • 文心一言 VS 讯飞星火 VS chatgpt (383)-- 算法导论24.5 3题
  • pgsql表分区和表分片设计
  • CTF-WEB: python模板注入
  • 能通过Ping命令访问CentOS 9 Stream,但在使用Xshell连接
  • ubuntu unrar解压 中文文件名异常问题解决
  • 使用SpringMVC+Layui操作excel的导入导出
  • 慢SQL优化方向
  • Android——画中画模式
  • js、vue、angular中的函数声明方式及特点
  • docker下迁移elasticsearch的问题与解决方案
  • 关于 C# (C Sharp)测试
  • Spring Boot技术在校园社团管理中的高效应用
  • Javascript的进阶部分(DOM)操作 !!
  • ssm023实验室耗材管理系统设计与实现+jsp(论文+源码)_kaic
  • 开源与商业的碰撞TPFLOW与Gadmin低代码的商业合作
  • 机器视觉:ROI在机器视觉中的作用
  • Spring学习笔记_24——切入点表达式
  • Pwn学习笔记(10)--UAF