【多线程】线程的等待通知机制-wait与notify

news2024/11/14 0:47:32

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


        我们都知道,线程在系统调度上是随机的,因此线程之间执⾏的先后顺序难以预知。但在实际开发中有时我们希望控制多个线程执行某个逻辑的先后顺序,就可以让后执行的逻辑使用wait,先执行的线程完成某些逻辑后,再通过notify唤醒对应的线程,从而使多个线程以一定的顺序运行。那么本篇文章就让我们深入地去探讨wait与notify的特点与应用吧

目录

一、wait()

二、notify()

三、notifyAll()

四、等待通知机制的简单应用

五、wait和sleep的对比

六、线程饿死


一、wait()

wait 做的事情:
  • 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.
注意: wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常
我们发现抛出了一个 “IllegalMonitorStateException”异常   大致意思就是“非法的锁状态异常”。就是说调用wait时,当前锁的状态是非法的(不正常的)。因为调用wait()后,会进行一个针对locker对象先解锁的操作,所以,必须要在synchronized代码块中,才能够使用wait(),因为显然要先上锁,才能够正常地解锁
wait 结束等待的条件:
其他线程调⽤该对象的 notify ⽅法告诉该对象不用再等了.
wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.

 wait()应用示例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized (locker){
            System.out.println("wait 之前");
            locker.wait();
            System.out.println("wait 之后");
        }

    }
}

 这时main线程就会进入阻塞等待的状态,等待其它线程的唤醒。

我们可以通过jconsole来观察到此时main线程的状态:


wait默认是 死等,这容易导致线程永久陷入阻塞状态,并不是非常合适的
因此wait还提供了带参数的版本,指定 超时时间
如果wait达到了设定的最大时间,即使还没有收到notify通知也 不会继续等待了,而是会重新恢复到 RUNNABLE状态,重新加入锁竞争的行列。
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized (locker){
            System.out.println("wait 之前");
            locker.wait(100000);
            System.out.println("wait 之后");
        }

    }
}

·注意·实际开发一般都会根据需要写入等待时间

执行wait要进行解锁,也要进行阻塞等待。

注意:这两步操作是 同时执行的(符合 原子性的)
那么如果不是原子性的会出现什么问题呢?

二、notify()

 notify方法是通过另一个线程,去通知处于WAITING状态的线程被唤醒

我们观察一下以下代码:

public class Main {
    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 in=new Scanner(System.in);
            in.next();  //此处输入内容不重要,主要是构成一个阻塞,保证t1能进入wait
            locker.notify();
            System.out.println("t2 notify 之后");
        });

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

我们可以发现又抛出了那个熟悉的“IllegalMonitorStateException”异常,也就是非法的锁状态异常,因为在多线程中,一个线程加锁,另一个线程不加锁,是没有意义的,不会产生任何阻塞效果。不光要对执行wait的线程加锁,对执行notify的线程也要加锁才行:

public class Main {
    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 notify 之前");
                Scanner in=new Scanner(System.in);
                in.next();  //此处输入内容不重要,主要是构成一个阻塞,保证t1能进入wait
                locker.notify();
                System.out.println("t2 notify 之后");
            }

        });

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

  • ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏完,也就是退出同步代码块之后才会释放对象锁。

三、notifyAll()

notify只是唤醒wait中的某个线程notifyAll会唤醒wait中的所有线程

不过一个一个唤醒,整个程序的执行是相对有序的,如果一下唤醒所有线程,这些线程就会开始无序的进行锁竞争

比如在面试的时候,可能有多个房间,每个房间一个面试官。所有候选人在大厅等待。

  • notify就相当于是让HR喊XXX进入某个房间面试
  • notifyAll就相当于HR喊某个房间有空位了,你们快去,先到先得。

显然notify会更加井然有序一些。因此在日常开发中,一般都会采用notify来一个一个唤醒

不过这时可能会有人担心,使用notify是不是得注意不能用太多了。当notify数量超过wait线程的数量是否会出现bug呢?

答案是不会的,我们把之前写的t2线程多加入几个notify试试:

最后的运行结果依然是没有问题的:

四、等待通知机制的简单应用

在前文中我们提到过,等待通知机制可以控制多个线程执行某个逻辑的先后顺序,接下来我们就来看看具体的实现方式吧:

场景描述:创建三个线程分别打印A、B、C,通过wait、notify来控制线程的打印顺序,先打印A,再打印B,最后打印C

public class Main {
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("A");
            synchronized (locker1){
                locker1.notify();
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("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();
    }
}

但我们会发现这样的代码可能会出现bug:

如果这段代码是先执行t2的wait,后执行t1的notify,那么就不会出现问题。但由于线程三个线程是并发执行的,所以一定概率上,t1先完成了打印和notify的操作,然后t2才执行wait,这样t2就会错过t1的通知,后面也没有线程来唤醒t2,就导致一直处于阻塞等待的wait状态了

解决方法也很简单,在t1中加入sleep,让t1先等待一段时间,保证t2,t3都进入wait状态后,再继续执行notify发送通知就行了:

运行结果:


五、wait和sleep的对比

wait(1000)与sleep(1000)看起来好像是十分相似的,都是让线程进入阻塞状态,但事实上它们是有着本质区别的

  • sleep就是固定时间的阻塞,不涉及唤醒操作。而wait则可以被notify提前唤醒
  • wait必须搭配synchronized使用,并且wait会先释放锁,同时进行阻塞等待
  • sleep与锁无关,不加锁也能正常使用。如果加了锁,sleep并不会释放锁,在阻塞期间其它线程是无法拿到锁的
  • wait 是 Object 的⽅法 ,sleep 是 Thread 的静态⽅法
  • wait主要是用来线程间通信的,而sleep就是单纯的阻塞操作

六、线程饿死

        由于多线程是抢占式并发执行的,在加锁后,多个线程会去同时竞争这把锁。假设这时线程a抢到了锁1,但是由于缺乏一些必要条件导致CPU并没有真正地执行线程a,这时线程a会释放锁1并重新开始与其它线程竞争锁1。如果在极端情况下,一直都是线程a抢到了锁1,其它线程就会一直处于阻塞状态,就像抢不到食物而被饿死了一样。这种现象就被称为“线程饿死”。这会大大降低线程的执行效率,导致所有的线程都无法按预期正常工作。

        这时也可以利用等待通知机制来解决这一问题。当线程a抢到锁后,会判断当前是否符合执行条件,如果不符合,就会主动通过wait方法释放锁进入阻塞队列,并放弃对锁(CPU资源)的竞争,这样就不会出现“线程饿死”问题了。直到其它线程判断符合其执行条件时,会调用notify方法通知阻塞线程a,此时线程a才会重新参与锁的竞争


那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

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

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

相关文章

基于JSP高校应届生就业信息管理系统的设计与实现(全网第一无二,阿龙原创设计)

博主介绍: ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台…

C#骑砍逻辑类Mod制作详细解说

前言: 最近在研究骑砍的mod,主要是想修改其中的逻辑部分,因此有了这篇帖子。 一,文件夹与XML配置 在Modules创建一个新文件夹,文件夹名称随意,不影响实际的读取。 文件夹下面的位置需要固定,因…

大模型学习路线:从新手到专家的全面指南,从零基础到精通,非常详细收藏我这一篇就够了

随着人工智能技术的飞速发展,特别是近年来深度学习领域的突破,大规模预训练模型(通常称为“大模型”)已成为推动自然语言处理(NLP)、计算机视觉(CV)等领域发展的关键力量。本文将为你…

CSS 嵌套元素的隐藏规则

简单介绍一下,在 HTML 和 CSS 中,元素大体分为 块级元素、内联元素(行内元素)、块级内联元素(行内块元素)。它们有着不同的嵌套规则和特殊之处。 1. 行内元素 行内元素特点:不独占一行、不可设…

06- Python的标识符

Python 标识符的知识点 简单地理解,标识符就是一个名字,就好像我们每个人都有属于自己的名字,它的主要作用就是作为变量、函数、类、模块以及其他对象的名称。 Python 中标识符的命名不是随意的,而是要遵守一定的命令规则&#xf…

Qt 调用MFC dll,动态库中有界面

一、创建MFC 动态库工程 下一步 创建 点击确定 二、创建接口 这个是系统创建的,改成自己的接口。 头文件: #ifndef __WEB_ENGINE__ #define __WEB_ENGINE__#ifdef __cplusplus extern "C" { #endif__declspec(dllexport) bool __stdcall Loa…

Datawhale AI 夏令营-CV竞赛-Task2

# Datawhale AI 夏令营 夏令营手册:从零上手CV竞赛 比赛:2024“大运河杯”数据开发应用创新大赛——城市治理赛道 代码运行平台:厚德云 赛题任务 本赛题的任务是开发智能识别系统,用于自动检测和分类城市管理中的违规行为。通…

Vue组件的好处和理解、基本使用、注意事项、组件嵌套、VueComponent理解和原型链

目录 1. 组件的好处和理解2. Vue组件的使用2.1 Vue中使用组件的三大步骤2.2 注意事项 4. 组件的嵌套5. VueComponent的理解6. VueComponent原型链 1. 组件的好处和理解 传统方式编写应用,存在2大问题: 依赖关系混乱,不好维护代码复用率不高…

中资优配:人气牛股10连板!

三大股指今日弱势轰动,均创2月初以来新低;小盘股较为生动,万得微盘股指数涨超1%;两市成交额再度萎缩至5000亿元下方;港股走势疲弱,两大股指均跌超1%。 具体来看,沪指在银行、酿酒等板块的拖累下…

ESP32-IDF http请求崩溃问题解决

文章目录 esp32s3 http请求崩溃问题代码讨论修正后不崩溃的代码 ESP32S3板子, 一运行http请求百度网站的例子, 就会panic死机, 记录下过程. esp32s3 http请求崩溃 一执行http请求的perform就会崩溃, 打印如图 ESP32-IDF 的http请求代码是根据官方demo来改的, 第一步先连接wi…

佰朔资本:大盘股和小盘股的区别?大中小盘股划分标准?

一般来说,大盘股:流通市值在500亿及以上,中盘股:流通市值在100亿~500亿之间,小盘股:流通市值在100亿及以下。 留意:流通市值是可以上市买卖流通的股数与股价乘积,总市值由流通市值与…

【项目源码】终于有人将打字游戏和编程英语结合起来啦!Java初学者的福音

Hello!各位彦祖,亦菲们!又是美好的一天!今天给大家分享一个Java项目源码:Java打字游戏项目源码! 看到这里,你可能会说! 一个破打字游戏有什么可神气的!!&…

OpenCV 图像处理中滤波技术介绍

VS2022配置OpenCV环境 关于OpenCV在VS2022上配置的教程可以参考:VS2022 配置OpenCV开发环境详细教程 图像处理中滤波技术 图像滤波是图像处理中的一种重要技术,用于改善图像质量或提取图像中的特定特征。以下是一些常见的图像滤波技术: 均…

LeetCode 热题100-41 二叉树的层序遍历

二叉树的层序遍历 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]示例 2&…

线上预订酒店订房小程序源码系统 多商家入驻 带完整的安装代码包以及搭建部署教程

系统概述 线上预订酒店订房小程序源码系统是一款基于微信小程序开发的酒店预订系统。它充分利用了微信小程序的便捷性和普及性,为用户提供了一个方便、快捷的酒店预订渠道。同时,该系统还支持多商家入驻,允许不同的酒店商家在同一个平台上展…

uniapp自定义头部导航栏布局(普通版)

H5与微信小程序 通过获取系统信息和获取胶囊按钮的信息&#xff0c;得到获取标题栏高度&#xff0c;成而做好自定义头部导航栏 在微信小程序可使用 但在H5就保错&#xff0c;就需要优化 <!-- 全局custom-nav-bar组件 --> <template><view class"customN…

【Docker】Dockerfile实列-Nginx镜像构建

一、镜像构建步骤 实验准备&#xff1a;导入centos7镜像&#xff08;因为现在docker镜像拉取不下了&#xff09; docker load -i centos-7.tar.gz 1、建立构建目录&#xff0c;编写构建文件 [rootdocker-node1 ~]# mdkir /docker [rootdocker-node1 ~]# cd /docker [rootdo…

发现一个程序员最强外设,助你高效开发早日摸鱼!

简介 最近公司的副屏有点问题&#xff0c;经常屏闪&#xff0c;无意中和媳妇儿吐槽了几句。没想到&#xff0c;生日的时候&#xff0c;居然收到了她的礼物&#xff1a; 看到「程序员专用」的时候&#xff0c;我很开心的对媳妇儿表示了感谢&#xff0c;但内心第一反应是&#x…

1DM+ v17.1 修改版 — 多线程下载管理工具(高效稳定)

1DM 是一款适用于安卓设备的下载管理工具&#xff0c;支持多线程下载&#xff0c;可以加快下载速度。具备自动识别下载链接、断点续传、下载任务管理和文件浏览等功能。此修改版由 Balatan 制作&#xff0c;无需 root 或 Lucky Patcher&#xff0c;禁用不必要的权限和功能&…

学习之SQL语句

SQL通用语法 1、SQL语句可以单行或者多行书写&#xff0c;以分号结尾 2、SQL语句可以使用空格或者缩进增强语句的可读性 3、MySQL数据库的SQL语句不区分大小写&#xff0c;关键字建议使用大写 4、注释&#xff1a; 单行注释&#xff1a;-- 注释内容 或 # 注释内容&#xff08;…