【多线程】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()只有被唤醒后,线程才能重新尝试获得锁,得到锁后才能继续执行

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenBayes 教程上新 | 清华大学强推!YOLOv10 实现更高效的目标检测

过去几年中,由于 YOLO 在计算成本和检测性能之间的有效平衡,它已经成为实时目标检测领域的主要范式。然而,YOLO 依赖于非极大值抑制 (NMS) 进行后处理,这阻碍了 YOLO 的端到端部署,并对推理延迟产生了不利影响。 YOLO…

SpringBootV12和mybatis全部知识点

框架: 快速开发项目的一个架子 ssh ssm spring --> applicationContext.xml配置文件(spring不是业务层,是管理其他框架的) springmvc --> springmvc.xml配置文件 (对应之前servlet) mybatis —> mybatis-config.xml配置文件(对应之前jdbc) —> springboot优化…

【AI技术的未来之路】从模型到应用,跨越超级应用陷阱,迈向个性化智能体

💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《热点时事》 期待您的关注 ​ 目录 引言 一、AI技术应用场景探索: 二、避免超级应用陷阱的策略: 三、个…

【STM32】MDK的编译过程及文件类型全解

1.编译过程简介 编译:MDK软件使用的编译器是armcc和armasm, 它们根据每个c/c和汇编源文件编译成对应的以“.o”为后缀名的对象文件(Object Code,也称目标文件), 其内容主要是从源文件编译得到的机器码,包含了代码、数据…

无线网卡怎么连接台式电脑?让上网更便捷!

随着无线网络的普及,越来越多的台式电脑用户希望通过无线网卡连接到互联网。无线网卡为台式电脑提供了无线连接的便利性,避免了有线网络的束缚。本文将详细介绍无线网卡怎么连接台式电脑的四种方法,包括使用USB无线网卡、内置无线网卡以及使用…

火柴棒图python绘画

使用Python绘制二项分布的概率质量函数(PMF) 在这篇博客中,我们将探讨如何使用Python中的scipy库和matplotlib库来绘制二项分布的概率质量函数(PMF)。二项分布是统计学中常见的离散概率分布,描述了在固定次…

Flask 邮件发送实例(代码直接可用)

关于Flask 邮件发送功能的代码实现,很多文章讲得并不清楚,往往学习视频才能讲清楚,我在这里出一个简单实例,直接告诉你各个配置具体对应的是什么意思以及如何获取。 1、实例 from flask import Flask from flask_mail import Ma…

快速掌握 ==== js 正则表达式

git 地址 https://gitee.com/childe-jia/reg-test.git 背景 在日常开发中,我们经常会遇到使用正则表达式的场景,比如一些常见的表单校验,会让你匹配用户输入的手机号或者身份信息是否规范,这就可以用正则表达式去匹配。相信大多数…

ISO 20000认证:驱动企业IT服务管理变革的利器

在信息技术驱动商业发展的今天,企业对高效、可靠和安全的IT服务需求日益增长。ISO 20000作为国际公认的IT服务管理标准,能够帮助企业在竞争激烈的市场环境中脱颖而出,实现IT服务管理的全面提升。本文将深入探讨ISO 20000认证如何帮助企业优化…

C++第三弹 -- 类与对象(上)

目录 前言一. 面向过程和面向对象的初步认识二. 类的引入三. 类的定义1.定义2. 命名规则建议 四. 类的访问限定符以及封装1. 访问限定符2.面试题3. 封装 五. 类的作用域六. 类的实例化七. 类的对象大小的计算八. 类成员函数this指针1. this指针的引出2. this指针的特性3. C语言…

最新版本的办公工具,你不来尝试一下吗?

前言 大家好,我是小雨,看到最近ONLYOFFICE更新了最新的版本,更新了一下当前版本来具体的测评一下,先来看看官网提供的各类更新信息,下面是我找到的三个主页,包括功能演示链接,官网连接以及专门…

能源电子领域2区SCI,版面稀缺,即将截稿,无版面费!

【SciencePub学术】今天小编给大家推荐1本能源电子领域的SCI!影响因子1.0-2.0之间,最重要的是审稿周期较短,对急投的学者较为友好! 能源电子类SCI 01 / 期刊概况 【期刊简介】IF:1.0-2.0,JCR2区&#xf…

羊大师:探索羊奶奥秘,解锁免疫力提升新篇章

在浩瀚的自然界中,羊奶以其独特的营养价值和健康益处,悄然成为提升免疫力的新宠。自古以来,羊奶就被视为珍贵的滋补佳品,而今,随着科学的深入探索,其提升免疫力的奥秘正逐渐揭开面纱。 羊奶中富含的免疫球蛋…

Avalonia开发实践(二)——开发带边框的Grid

一、开发背景 在实际开发工作中,常常会用到Grid进行布局。为了美观考虑,会给每个格子加上边框,如下图: 原生的Grid虽然有ShowGridLines属性可以控制显示格子之间的线,但线的样式不能定义,可以说此功能非常…

java中 使用数组实现需求小案例(二)

Date: 2024.07.09 16:43:47 author: lijianzhan 需求实现: 设计一个java类,java方法,使用Random函数,根据实现用户输入随机数生成一个打乱的数组。 package com.lin.java.test;import java.util.Arrays; import java.util.Rando…

TAGE predictor

参考文档:分支预测算法(一):TAGE|SunnyChen的小窝 TAGE的基础概念 TAGE是现今最经典的分支预测算法,TAGE及其后续的变体都是当今高性能微处理器的分支预测算法基础。因此,要聊分支预测算法的话题必定绕不开…

uniapp内置组件uni.navigateTo跳转后页面空白问题解决

文章目录 导文空白问题 导文 在h5上跳转正常 但是在小程序里面跳转有问题 无任何报错 页面跳转地址显示正确,但页面内容为空 空白问题 控制台: 问题解决: 方法1: 可能是没有注册的问题,把没注册的页面 注册一下。 方…

数据库基础练习4

准备 create table dept (dept1 int ,dept_name varchar(11)) charsetutf8; create table emp (sid int ,name varchar(11),age int,worktime_start date,incoming int,dept2 int) charsetutf8;insert into dept values(101,财务),(102,销售),(103,IT技术),(104,行政);INSERT …

WANGLS

DHCP 动态主机配置协议 原理 网络 网络是双向的,网络是有方向的 广播;广播是由种类的,广播是有范围的的 租约的建立——租约的相应、租约的选择——租约的完成 租约的建立:租约的请求 有客户端发出 DHCP discover 广播、寻找服务器 租约的响应 收到响应,不是服务器,…

模块一SpringBoot(一)

maven记得配置本地路径和镜像 IJ搭建 SpringIntiallizer--》将https://start.spring.io改成https://start.aliyun.com/ 项目结构 Spring有默认配置, application.properties会覆盖默认信息: 如覆盖端口号server.port8888