【多线程】wait()和notify()

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述

文章目录

  • 1. 为什么需要wait()方法和notify()方法?
  • 2. wait()方法
    • 2.1 wait()方法的作用
    • 2.2 wait()做的事情
    • 2.3 wait()结束等待的条件
    • 2.4 带参数的wait方法 —— wait(timeout)
    • 2.5 wait()必须写在 synchronized 代码块里
  • 3. notify()方法
    • 3.1 notify()方法的作用
    • 3.2 notify()方法的用法
    • 3.3 notify()方法必须写在 synchronized 代码块里
    • 3.4 notifyAll() 方法
  • 4. 面试题 —— join()、sleep()方法和wait()方法的对比
    • 4.1 join()和wait()方法的区别
      • 4.1.1 从java包来看
      • 4.1.2 从作用效果来看
    • 4.2 sleep()方法和wait()方法的对比
      • 4.2.1 相同点
      • 4.2.2 不同点

通过之前的学习,我们知道线程的调度是无序的,随机的,但在一定的需求场景下,我们希望线程是有序执行的~
join()方法算是一种控制顺序的方式,但是功效是有限的,所以接下来,本期内容具体介绍两种方法:wait()和notify(),合理的协调多个线程之间的执行先后顺序

1. 为什么需要wait()方法和notify()方法?

举一个栗子~ 假如小万和小丁约好一起去吃自助餐,小丁进去之后发现自己喜欢吃的菜都被拿光了!!!而此时工作人员还没有进行补货~ 发生的一系列故事
在这里插入图片描述
解释说明
这里想要的菜看作是锁,而5个人看作是竞争锁的5个线程,小丁在这个菜面前进进出出,其实并没有实质性地释放锁,但由于这盘菜始终没有工作人员补货,小丁也拿不到自己想要吃的菜,小丁就会陷入忙等,而其它人又竞争不到这盘菜,可看作是线程竞争不到CPU资源,其他人就处于一直在阻塞的状态,也什么事情都干不了

所有线程都可以竞争这个锁,这里就会出现一个极端的情况:线程刚释放锁又是该线程获得锁,发现里面没有货,又释放又进去,一直循环着,而其它线程拿不到锁,处于阻塞状态啥也干不了,导致一个问题 —— 线程饿死

这里引入了一个新的概念—— 线程饿死,通过上述案例,总结为:
线程饿死】指一个或多个线程由于某种原因无法获取所需的执行机会,导致它们无法继续正常执行,从而被阻塞在某个状态,不能完成其任务,这种情况通常是优先级设置不当导致的

上述问题如何解决呢?
有些同学可能会觉得线程有记账信息,可以避免此问题,但实际上,并不能!!!
线程的记账信息其实是一个比较宏观的东西,它需要多个线程多运行一段时间才能生成,对于上面的案例,线程饿死,同一个线程进进出出的情况是一瞬间的事,故线程的记账信息无法解决~

正确:使用wait()和notify()可以有效解决上述问题!!! 上述情况如下:
在这里插入图片描述
解释说明
小丁站在菜面前发现没菜时,就先wait()释放锁,并进行阻塞等待,即暂时不参与CPU调度,不参与锁竞争,当服务员进行补菜,通知notify小丁有菜了,可以去拿菜了,小丁再去重新竞争资源拿到菜,小丁在阻塞等待时,小万和其他3个人,也是要拿这盘菜,但是条件不满足,也是wait()等待就行
wait()】发现条件不满足或是时机不成熟时,线程就先阻塞等待
notify()】其它线程构造一个成熟的条件,就可以唤醒该线程,唤醒后就可以参与所竞争了

协调好多个线程的执行顺序是很重要的,其实在日常生活中,还有很多这样的案例,为了更深刻理解,再比如小丁还喜欢打篮球,球场上的每个人都是独立的"执行流" ,即每个人可当作是一个线程,完成一个具体的进攻得分好几个动作, 需要多个人一起相互配合,必须按照一定的顺序执行,例如1号球员先传球,2号球员拿到球才能扣篮,没拿到球时只能wait()阻塞等待,对应线程中,线程1 先 “传球” 完成自己的任务,通知notify()2号球员已经传球了,线程2 拿到球才能 "扣篮"才能完成自己的任务,是讲究顺序的,wait()和notify()就是解决上述问题的

wait()和notify()的作用即合理的协调多个线程之间的执行先后顺序

2. wait()方法

2.1 wait()方法的作用

作用:让某个线程先暂停下来,等一等

wait()方法的初心就是阻塞等待,让线程进入 WAITING 状态!不过我们要区分开来,不要把概念弄混淆了,
wait()方法导致阻塞,竞争锁也可以导致阻塞,这是两种不同线程进入阻塞的方式!

注意】wait()和notify()是Object的方法,只要是个类对象,不是内置类型(也叫基本数据类型),都可以使用wait()和notify()方法

2.2 wait()做的事情

1)释放当前的锁
2)使当前执行代码的线程进行阻塞等待(即把线程放到等待队列中)
3)满足一定条件时收到通知,被唤醒,同时重新尝试获取这个锁

2.3 wait()结束等待的条件

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

2.4 带参数的wait方法 —— wait(timeout)

wait() 方法也提供了一个带参数的版本,timeout参数即为指定的最大等待时间
不带参数的wait()即为死等,只有notify()方法能唤醒它
带参数的wait(timeout),则为等到最大时间还没有通知,就自己唤醒自己

2.5 wait()必须写在 synchronized 代码块里

这里需要重要注意: wait()必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait前:");
        obj.wait();
        System.out.println("wait后:");
    }
}

上述代码中,直接调用obj.wait(),并没有进行加锁,运行后,抛出 IllegalMonitorStateException,即非法的锁状态异常,运行结果如下:
在这里插入图片描述
为什么会出现这种情况?
回顾wait()需要做的事情,先进行解锁!!! 如果这把锁都没获取到,就尝试解锁,就会产生异常!(在现实生活中,如果你开门,都没有这把锁,就尝试开锁,东西都没有,咋进行开锁操作捏!所以就会抛出异常)

所以在使用wait()方法前,必须要先进行加锁,就是把wait()写在 synchronized 代码块里面!!!

注意事项同时需要注意,加锁的锁对象必须要和wait()的锁对象是同一个,如果加锁对象和调用wait()对象不是同一个,也会抛出IllegalMonitorStateException 异常!!!

正确使用wait()方法如下:

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }
}

打印结果如下:

在这里插入图片描述
打印结果显示一直在等待中,分析可以得到,在执行到 object.wait() 方法后就一直等待下去,但是程序肯定不能一直这么等待下去呀,这个时候就需要使用到另外一个方法唤醒的方法notify()!!! 下面notify()方法闪亮登场~

3. notify()方法

3.1 notify()方法的作用

作用:把该线程唤醒,使其能够继续执行

3.2 notify()方法的用法

1)notify()方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,同时使它们重新获取该对象的对象锁
2)如果有多个线程等待,则由线程调度器随机挑选出一个呈 wait 状态的线程,并不遵循"先来后到"的原则,仍然是随机的
3)在notify()方法后,当前线程不会立刻释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized 代码块之后才会释放对象锁

在接下来的代码里,我们会更深刻理解以上3点

3.3 notify()方法必须写在 synchronized 代码块里

这里需要重要注意: notify()也必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常

注意事项】同时需要注意,必须先执行wait()方法,然后再执行notify()方法,此时才会有效果,试想一下,如果现在还没有执行wait(),就执行notify()方法,这不就相当于一炮打空嘛~没啥效果!没有起到实际作用,虽然没有额外的副作用,也不会抛出异常,但是代码的功能就不能正确执行了

使用wait()和notify()方法,更好理解wait()和notify()方法的用法,代码如下:

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();

        Thread t1 = new Thread(() -> {
            try {
                System.out.println("wait开始");
                synchronized (obj) {
                    obj.wait();
                }
                System.out.println("wait结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();

        Thread.sleep(1000); //保证t1先启动,wait()先执行

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("notify开始");
                obj.notify();
                System.out.println("notify结束");
            }
        });

        t2.start();
    }
}

分别创建 t1 和 t2 两个线程,且它们对同一个对象加锁,在此代码中让 t1 线程中执行wait(),t2 线程中执行notify(),先后启动t1和t2线程,观察代码运行结果:

在这里插入图片描述解释说明
1)t1 线程先执行,执行到wait()方法,t1 线程的锁就被wait()方法释放,且 t1 线程自身就阻塞等待了,
2)1s之后 t2 线程开始执行,执行到notify()方法,就会通知 t1 线程,t1 线程被唤醒,继续执行
需要注意的是,notify()方法在 synchronized 代码块内部,因此,只有等 t2 线程释放锁之后,t1 线程才能再竞争到锁,t1 才能继续往下执行,所以先打印的是"notify()结束",再是打印"wait结束"

在上述代码中,虽然是 t1 线程先执行的,但是可以通过wait()方法、notify() 方法的控制,让 t2 线程先执行一部分逻辑,执行完后,t2 线程通过 notify()方法唤醒 t1 线程,使 t1 线程继续往下执行!这正是它们的意义,wait()方法、notify() 方法可以合理的协调多个线程之间的执行先后顺序,使线程执行顺序变得可控起来!

3.4 notifyAll() 方法

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

存在这样一个情况:可以有多个线程,等待同一个对象,比如在 t1、t2、t3 线程中,都调用 object.wait()
1)此时在 main 线程中调用 object.notify(),就会随机唤醒上述三个线程中的一个,而另外两个线程仍然是处于 WAITING 状态,
2)但是如果调用 object.notifyAll(),此时就会把上述三个线程全部唤醒,此时这三个线程就会重新竞争锁,再依次执行
注意】此时需要三个线程都wait()等待,再通知,不然又要空打一炮啦

4. 面试题 —— join()、sleep()方法和wait()方法的对比

4.1 join()和wait()方法的区别

4.1.1 从java包来看

join()方法
join()方法在java.lang.Thread 声明
wait()方法
wait()方法在java.lang.Object 声明

4.1.2 从作用效果来看

join()方法
当在 t1 线程中调用了t2.join(),这是让 t1 线程等待 t2 线程全部执行完毕才能再执行,这使得线程之间的执行从"并行"变成"串行"
wait()方法
wait()和notify()方法搭配使用,可以让 t2 线程执行一部分,再让 t1 线程执行一部分,t1 线程执行一部分再让 t2 线程执行一部分…

4.2 sleep()方法和wait()方法的对比

4.2.1 相同点

wait()有一个带参数版本,用来体现超时时间,超过这个时间会被自动唤醒,此时和sleep()方法类似
同时,wait()和sleep()方法都能提前被唤醒

4.2.2 不同点

1)最大的区别:在于两者的初心不同,即设计这个东西到底要解决啥问题的不同
【sleep()】sleep()方法单纯是让当前线程休眠一会
【wait()】wait()解决的是线程之间的顺序控制
2)进一步的,实现或使用上,也是有明显区别的,wait()需要搭配锁使用,而sleep不需要,sleep()方法是让程序按照指定时间短暂休眠让出CPU给其它线程,到时间自动恢复,而不带参数的wait()只有被唤醒后,线程才能重新尝试获得锁,得到锁后才能继续执行

本期内容主要讲解如何在实际中,控制线程调度的顺序~
💛💛💛本期内容回顾💛💛💛
在这里插入图片描述
✨✨✨本期内容到此结束啦~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/787983.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IDEA新建项目并撰写Java代码的方法

本文介绍在IntelliJ IDEA软件中,新建项目或打开已有项目,并撰写Java代码的具体方法;Groovy等语言的代码也可以基于这种方法来撰写。 在之前的文章IntelliJ IDEA社区版在Windows电脑中的下载、安装方法(https://blog.csdn.net/zheb…

01 | 基础架构:一条SQL查询语句是如何执行的?

此系列文章为极客时间课程《MySQL 实战 45 讲》的学习笔记! 引言 在了解 SQL 查询语句如何执行之前,先了解下MySQL 的基本架构示意图。 MySQL 分为 Server 层和引擎层。 Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 M…

揭秘GPT-4o:未来智能的曙光

引言 近年来,人工智能(AI)的发展突飞猛进,尤其是自然语言处理(NLP)领域的进步,更是引人注目。在这一背景下,OpenAI发布的GPT系列模型成为了焦点。本文将详细探讨最新的模型GPT-4o&a…

【刷题汇总 -- 求最小公倍数、数组中的最长连续子序列、字母收集】

C日常刷题积累 今日刷题汇总 - day0081、求最小公倍数1.1、题目1.2、思路1.3、程序实现 -- 穷举法1.2、程序实现 -- 辗转相除法 2、数组中的最长连续子序列2.1、题目2.2、思路2.3、程序实现 3、字母收集3.1、题目3.2、思路3.3、程序实现 4、题目链接 今日刷题汇总 - day008 1、…

基于STM32的智能加湿器

1.简介 基于STM32的加湿器发展前景非常乐观,这主要得益于其在技术、市场需求、应用场景以及政策支持等多方面的优势。STM32微控制器具备强大的处理能力和丰富的外设接口,能够实现精确的湿度监测和智能化控制。基于STM32的加湿器可以根据环境湿度自动调节…

Spark实现电商消费者画像案例

作者/朱季谦 故事得从这一张图开始说起—— 可怜的打工人准备下班时,突然收到领导发来的一份电商消费者样本数据,数据内容是这样的—— 消费者姓名|年龄|性别|薪资|消费偏好|消费领域&#x…

使用各向异性滤波器和图像处理方法进行脑肿瘤检测(MATLAB)

医学图像分割一直以来都是计算机辅助诊断领域的研究热点。在医学图像的处理和分析中,对图像中感兴趣区域的准确分割尤其关键。要对感兴趣区域进行分类识别,首先要从图像中把感兴趣区域精确分割出来,然后有针对性地对感兴趣区域提取特征并分类…

使用clion刷leetcode

如何优雅的使用clion刷leetcode 安装插件:LeetCode Editor) 插件配置: 这样我们每打开一个项目,就会创建类似的文件 我们的项目结构: 我们在题解文件中导入头文件myHeader.h并将新建的文件添加到cmakelists.txt文件,…

初识CPlusPlus

前言 也是好久没写博客了,那些天也没闲着,去练题去了。实际上练题也可以写练题的博客,但是觉得太简单了些,于是就没有继续写下去。如今又回来写博客,是因为有整理了新的知识C。内容不算多,大多数都是书本上…

接口测试工具Apifox使用以及多环境的配置

下载 Apifox - API 文档、调试、Mock、测试一体化协作平台 - 接口文档工具,接口自动化测试工具,接口Mock工具,API文档工具,API Mock工具,API自动化测试工具 安装 正常安装 , 微信扫码注册 apifox中创建项目 安装idea插…

数学建模美赛入门

数学建模需要的学科知识 高等数学线性代数 有很多算法的掌握是需要高等数学和线代的相关知识 如:灰色预测模型需要微积分知识;神经网络需要用到导数知识;图论和层次分析法等都需要用到矩阵计算的相关知识等; 概率论与数理统计&am…

Xubuntu24.04之设置高性能模式两种方式(二百六十一)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+AOSP…

YOLOv8改进 在通道维度上引入注意力机制CPCAttention

一、CPCAttention论文 论文地址:2306.05196 (arxiv.org) 二、CPCAttention结构 Channel prior convolutional attention (CPCA)用于图像分类和目标检测任务中。CPCA能够在卷积神经网络中引入通道相关性,并通过自适应地学习到每个通道的权重,从而提升模型的性能。 CPCA的关…

纯前端如何实现Gif暂停、倍速播放

前言 GIF 我相信大家都不会陌生&#xff0c;由于它被广泛的支持&#xff0c;所以我们一般用它来做一些简单的动画效果。一般就是设计师弄好了之后&#xff0c;把文件发给我们。然后我们就直接这样使用&#xff1a; <img src"xxx.gif"/>这样就能播放一个 GIF …

通过rpmbuild构建Elasticsearch-7.14.2-search-guard的RPM包

系列文章目录 rpmbuild从入门到放弃 search-guard插件使用入门手册 文章目录 系列文章目录前言一、资源准备二、spec文件1.基础信息2.%prep3.%Install4.%file5.%post6.%postun 三、成果演示1.执行构建过程图示例2.执行安装RPM包示例3.进程检查4.访问esApi 总结 前言 不管是源…

Linux--深入理与解linux文件系统与日志文件分析

一、文件与存储系统的 inode 与 block 1.1 硬盘存储 最小存储单位:扇区( sector )每个扇区存储大小:512 字节1.2 文件存取--block block(块),每个 block 块大小为:4k由连续的八个扇区组成一个 block 块是文件索引最小的单位每个 block 块中包括:文件数据文件数据:就…

LeetCode(2)-反转链表、删除链表中等于val的节点、返回链表中的中间节点

一、反转链表 . - 力扣&#xff08;LeetCode&#xff09; 解法1&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ typedef struct ListNode ListNode; struct ListNode* reverseList(struct ListN…

ORA-12537: TNS:连接关闭/Io 异常: Got minus one from a read call

在另外一个数据库建立dblink的时候&#xff0c;发现执行命令报错&#xff1a; 被连接的数据库我也上去过&#xff0c;用工具尝试登陆也报错&#xff1a; IO Error: Got minus one from a read call, connect lapse 1 ms., Authentication lapse 0 ms. Got minus one from a …

Redis过期策略

过期的key集合 Redis会将每个设置了过期时间的key放入到一个独立的字典中&#xff0c;以后会定时遍历这个字典来删除到期的key。除了定时遍历之外&#xff0c;他还会使用惰性策略来删除过期的key&#xff0c;所谓惰性策略就是在客户端访问这个key的时候&#xff0c;redis对key…

生成随机密码

生成8位无重复的密码&#xff08;可以包含数字、大小写字母&#xff09; import random import string character string.digits string.ascii_letters password .join(random.sample(character, 8)) print(f"生成的随机密码为:{password}")