【Java多线程编程】wait与notify方法详解

news2024/11/19 20:20:49

 前言

我们知道,线程的调度是无序的,但有些情况要求线程的执行是有序的。因此,我们可以使用 wait() 方法来使线程执行有序。

本期讲解 Java 多线程中 synchronized 锁配套使用的 wait 方法、notify方法和notifyAll方法,以及 wait 方法与 sleep 方法之间的区别、为什么要使用 wait 和 notify 方法。

目录

为什么要使用wait()方法和notify()方法?

1. wait()方法

2. notify()方法

3. wait()和notify()方法的使用

4. notifyAll()方法

5. wait()和sleep()的区别

为什么要使用wait()方法和notify()方法?

当我们的 Java 代码使用 synchronized 进行加锁时,会出现线程之间抢占资源的情况。这样就会导致某一个线程不符合条件却反复抢到资源,其他线程参与不了资源。因此得使用 wait() 方法与 notify() 方法来解决该问题。

通过现实生活中的经历举一例子:

把三个线程比做人,把一台 ATM 机比作锁(synchronized)。当这三个线程去取钱时,线程1优先进入了 ATM 机里面取钱。

当 ATM 里面没有钱时,线程1就出了 ATM 机。但由于线程离开了 ATM 机后,会一直与线程2和线程3抢占 ATM 机,因此会造成一个极端的后果,就是线程1一直进入 ATM 机然后出 ATM 机,并且一直循环下去。

以上例子,线程1发现 ATM 没钱可取,却还是反复进出 ATM 这样这样其他线程就无法尝试取钱,对应的就是多线程中的多个线程竞争锁(synchroized)的情况,如何解决以上问题。

使用 wait 方法和 notify 方法。当 ATM(synchronized) 内使用了 wait 方法,线程1取不了钱就会取消锁状态并且处于等待状态,当其他线程进入 ATM 机并且取到了钱这时候就可以使用 notify 方法唤醒 线程1的等待状态,那么线程1又可以进行取钱操作,也就是进行锁的竞争。

在使用 wait 方法后,线程1发现 ATM 里面没有钱可取,就会通过 wait 方法来释放锁并且进行阻塞等待(也就是暂时不参与 CPU 的调度、锁的竞争),这个时候线程2和线程3就能很好的参与取钱这个操作了。

当其他线程 使用 notify 方法时,发现 ATM 里面又有钱可取了。因此就会唤醒线程1的阻塞等待,这时线程1又可以参与 ATM(锁) 的竞争。直到,所有的线程都取到钱为止。


那么使得上述三个线程能供协调的完成取钱这个工作,会用到三个方法:

  • wait() 方法/带参数的wait()方法 - 让当前线程进入等待阻塞状态
  • notify() 方法 / notifyAll() 方法 - 唤醒当前对象上等待的线程

注意:wait,notify、notifyAll都是 Object 类中的方法。


1. wait()方法

wait 方法使用后:会把当前的执行的线程进行等待阻塞,然后释放当前线程的锁状态,当满足了一定条件后就被唤醒重新尝试获取这个锁

wait 结束条件的为:

  1. 其他线程调用该对象的 notify 方法,
  2. wait 等待时间超时(wait 方法提供了一个带有参数的版本,可以指定等待时间)
  3. 其他线程调用该等待的线程的 interrupt 方法,导致 wait 抛出 InterruptedException 异常

解释interrupt(),在一个线程中调用另一个线程的interrupt()方法,即会向那个线程发出信号—线程中断状态已被设置。至于那个线程何去何从,由具体的代码实现决定。


wait 和 notify 方法是 Object 类里面的方法,只要是一个类对象都能调用这两个方法。因此,我们可以写出以下代码:

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("Hello object");
        object.wait();
        System.out.println("object结束");
    }

运行后打印:

以上代码运行后打印出一个非法的警告:非法的锁状态异常,因为 wait 方法必须要搭配 synchronized 来使用,脱离了 synchronized 的前提下 使用 wait 就会出现报错。


2. notify()方法

notify 方法是唤醒等待的线程,也就是唤醒调用了 wait 方法的线程。notify 方法作用:

  1. notify 方法也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
  2. 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  3. 如有多个线程处于等待,则线程调度器会随机挑选一个调用 wait 状态的线程。
  4. 在调用 notify 方法后,当前线程不会立马释放该对象的锁,要等当前调用 notify 方法的线程执行完毕后,才会释放该对象的锁。

在理解 wait 方法和 notify 方法的作用以及使用方法后,下面我们来看下 wait 方法和 notify 方法的结合使用。 


3. wait()和notify()方法的使用

代码案例:使用 notify() 方法唤醒 thread1线程。

  • 实例化一个 Object 类的对象,调用 wait 和 notify 方法都是用该对象的引用 object 来调用。
  • 创建两个线程:线程1和线程2,线程1执行两条语句,线程2也执行两条语句。
  • 线程1内使用 object 来调用 wait 方法(两条语句中间调用)
  • 线程2内使用 object 来调用 notify 方法(两条语句中间调用)

因此,有以下代码:

    public static void main(String[] args) {
        Object object = new Object();
        Thread thread1 = new Thread(()-> {
            synchronized (object) {
                System.out.println("thread1开始");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1结束");
            }
        });
        thread1.start();//启动thread1线程

        Thread thread2 = new Thread(()-> {
            synchronized (object) {
                System.out.println("thread2开始");
                object.notify();
                System.out.println("thread2结束");
            }
        });
        thread2.start();//启动thread2线程
    }

运行后打印:

以上代码,输出顺序与需求有所差异,但最终还是达到了效果。

造成输出顺序的不规则原因为:

当 thread1 线程被 wait 前打印了语句“thread1开始”,thread2 线程 中调用了 notify 方法,这时会唤醒 thread1 线程,但是前提得执行完 thread2 中的内容“thread2开始”、“thread2结束”这两个条语句。随后才输出被唤醒的 thread1 线程中的“thread1结束”语句。

当然,既然这样为啥我们不使用 join() 方法呢,thread1 线程完全执行完毕,再执行 thread2线程呢?具体情况具体分析,当我们的代码需求满足使用 join() 方法时,我们就使用 join() 方法。

对应上述代码,join() 方法会使 thread1 线程执行完毕后再执行 thread2 线程。而 wait() 和 notify() 方法会使 thread1 线程执行一部分后,执行 thread2 线程,执行完 thread2 一部分代码后,再执行thread1 线程。这样就满足了特定的条件,类似于上文中线程取钱情况。大家可以自行尝试一番。

注意,wait() 方法的初心就是为了等待、阻塞的效果。在 synchronized 内调用 wait() 方法,得按 Alt+Enter 这两个组合键来 try/catch 异常。


4. notifyAll()方法

notifyAll() 方法是用来唤醒当前对象的所有调用 wait() 的线程。案例:

  • 有三个线程,线程1为thread1、线程2为thread2、线程3为thread3
  • thread1 中输出两条语句“thread1开始”、“thread1结束”
  • thread2 中输出两条语句“thread2开始”、“thread2结束”
  • thread1 和 threa2 在两条语句中间通过 Object 类的引用调用 wait() 方法造成阻塞
  • thread3 线程通过 Object 类的引用调用 notifyAll() 方法唤醒所有的阻塞

因此,前两个线程都通过 Object 类的引用调用了 wait() 方法造成阻塞,最后一个线程调用 notifyAll() 则唤醒了所有调用 wait() 方法的线程,如以下代码:

public static void main(String[] args) {
        Object object = new Object();//实例化一个Object类的对象
        Thread thread1 = new Thread(()->{
            synchronized (object) {
                System.out.println("thread1-开始");
                try {
                    object.wait();//thread1中调用wait方法
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1-结束");
            }
        });//创建thread1线程
        thread1.start();//启动thread1线程
        
        Thread thread2 = new Thread(()->{
            synchronized(object) {
                System.out.println("thread2-开始");
                try {
                    object.wait();//thread2调用wait方法
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2-结束");
            }
        });//创建thread2线程
        thread2.start();//启动thread2线程
        
        Thread thread3 = new Thread(()->{
            synchronized (object) {
                object.notifyAll();//thread3中调用notifyAll方法
                System.out.println("thread3调用了notifyAll方法");
            }
        });//创建thread3线程
        thread3.start();//启动thread3线程
    }

运行后打印:

以上代码,通过 notifyAll() 方法唤醒了所有等待的线程。如果我把 notifyAll() 方法替换为 notify() 方法,此时就会随机唤醒一个正在等待的线程。如以下打印结果:

通过上面截图,我们可以观察到随机唤醒的是 thread1 线程。 


5. wait()和sleep()的区别

wait 与 sleep 之间的区别:

  1. wait() 方法是 Object 类底下的方法,sleep() 方法是 Thread 类底下的静态方法。
  2. wait()方法是搭配 synchronized 来使用的,而 sleep() 则不需要。
  3. 核心区别,初心不同,wait() 方法是为了避免线程之前的抢占资源(解决线程之间的顺序控制),而 sleep() 方法是为了让线程休眠特定的时间。
  4. wait() 方法有一个带参数的写法是用来体现超时的提醒(避免死等),因此用起来就感觉和 sleep() 方法一样。

案例:

有两线程,main 线程与 thread 线程,main 线程内包含 thread 线程,main 线程内有“Hello main”语句, thread 线程内有“Hello thread”语句。

在 main 线程内创建一个 thread 线程,并且在 thread 线程内使用 Object 类对象调用带参的 wait() 方法,并设置参数 为2000。

在main 方法内使用 Object 类对象调用 notify() 唤醒 thread 线程。使得输出 main 线程内语句后停顿两秒输出 thread 线程内语句。

有以下代码:

public static void main(String[] args) {
        Object object = new Object();//实例化一个Object类对象
        Thread thread = new Thread(()->{
            synchronized (object) {
                try {
                    object.wait(2000);//thread调用了带参wait方法,停顿了两秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Hello thread");
            }
        });//创建thread线程
        thread.start();//启动thread线程

        synchronized (object) {
            object.notify();//main方法内调用notify方法
        }
        System.out.println("Hello main");
    }

运行后打印:

输出“Hello main”语句后停顿了两秒,输出“Hello thread”线程。


重点: 

  • wait、notify、notifyAll都是 Object 类的方法
  • wait、notify、notifyAll 必须搭配 synchronized 关键字来使用
  • 不带参数的 wait 方法会造成死等、带参数的 wait 方法则不会
  • wait 方法的初心就是为了线程处于等待、阻塞状态
  • notify 方法的初心就是为了唤醒同一对象调用 wait 方法的随机一个线程
  • notifyAll 方法的初心就是为了唤醒同一对象调用 wait 方法的所有线程

🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。

🗃️文章收录于:Java多线程编程

🗂️JavaSE的学习:JavaSE

🗂️Java数据结构:数据结构与算法

 

 本期博文到这里就结束了,感谢点赞、评论、收藏、关注~

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

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

相关文章

jQuery 操作 DOM 及 CSS

jQuery 操作 DOM 及 CSS 1. jQuery 修改|获取页面元素文本及属性值 text() - 设置或返回所选元素的文本内容 html() - 设置或返回所选元素的内容(包括 HTML 标签) val() - 设置或返回表单字段的值 attr() - 方法用于获取属性值。 1.1 举例&#xff…

win11亮度条消失解决方法之一

一、前言 1、本人电脑情况:联想小新Pro16,win11家庭版 2、联想技术工程师三种方式联系方式(需提供电脑背后的编号): 1) 通过预装的联想电脑管家,获取在线人工专家支持(管家链接 h…

【ETH】以太网----PHY芯片LAN8720A----电路原理图

一、LAN8720A----简介 LAN8720A 是低功耗的 10/100M 以太网 PHY 层芯片,I/0 引脚电压符合EEE802.3-2005 标准,支持通过 RMI 接口与以太网 MAC 层通信,内置 10-BASE-T/100BASE-TX 全双工传输模块,支持 10Mbps 和 100Mbps。 LAN87…

SpringMVC的请求与响应(请求映射路径、请求参数、日期类型参数传递 、响应json数据)

文章目录 1,PostMan工具的使用1.1 PostMan简介1.2 PostMan安装1.3 PostMan使用1.3.1 创建WorkSpace工作空间1.3.2 发送请求1.3.3 保存当前请求 2,请求与响应2.1 设置请求映射路径2.1.1 环境准备2.1.2 问题分析2.1.3 设置映射路径步骤1:修改Controller步骤…

Java基础面试题突击系列2

👩🏻 作者:一只IT攻城狮 ,关注我不迷路 ❤️《java面试核心知识》突击系列,持续更新… 💐 面试必知必会学习路线:Java技术栈面试系列SpringCloud项目实战学习路线 📝再小的收获*365天…

以ChatGPT写诗为例,教你如何用AI软件创新性提问?

想用AI软件创作出动人的诗篇吗? ChatGPT 是一款人工智能软件,可以帮助你创作鼓舞人心的诗歌。它为您提供了一个强大的平台来探索您的创意方面。通过一组简单的问题,您只需点击几下就可以生成令人惊叹的诗歌。 要想让机器写诗,你…

低代码开发——进最热的赛道,啃最硬的骨头

你开足马力提了一串需求,需要招聘IOS和Android工程师、前端测试,PM等人员共同完成;这意味着开发者坐下来一行一行的敲击,并不断测试修改直到上线。 这个过程短则半年,长则数年,才会给到你一个满意的产品。…

基于 Web 的作物生长监控系统的

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 本毕业设计在对 A 基地进行调研的基础上。设计并开发了一套基于 Web 的作物生长监控系统,该系统由软件和硬件两部分组成。硬件部分采用了已开发好的多点温湿度采集系统,软件部分采用 Java 开发语言、HT…

回应小伙伴们的咨询,5款好用的小软件

最近陆陆续续收到好多小伙伴的咨询,这边也是抓紧时间整理出几个好用的软件,希望可以帮到大家。 1.RSS阅读器——Tabby Tabby是一款简洁美观的RSS阅读器。它可以自动抓取并订阅你喜欢的博客和网站更新。这个工具拥有简洁的界面和强大的功能,可以过滤和分…

零信任---ZTN

零信任是一种以身份为中心的新一代网络安全防护理念,通过持续的身份认证、环境状态采集、持续信任评估、动态访问控制,并遵循最小权限原则,实现在不可信网络中构筑可信的访问通道。 传统场景中,企业的安全都是在以防火墙为边界的。…

WhatsApp Business 多人使用终极指南

今时今日,几乎每个人的手机上都安装了 WhatsApp,不少电商更会依赖 WhatsApp作为和客户沟通的主要渠道。但对有一定规模的店铺来说,WhatsApp绑定一个号码和设备的设定实在很不方便。如何才能用WhatsApp Business批量处理客户查询呢&#xff1f…

Linux下安装MySQL8

一、安装MySQL8 1、下载 官网:https://dev.mysql.com/downloads/mysql/ 服务器wget下载 cd /data wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.28-1.el7.x86_64.rpm-bundle 2、解压 tar -xvf mysql-8.0.28-1.el7.x86_64.rpm-bundle.tar 3、…

十进制转二进制/八进制/十六进制

首先,我们有一个十进制的数字**(21)D**,其他博主都是用除法来算,如果数字小还能适用,如果数字大,就不适用了 以下是我的方法: 十进制转二进制: 通过上图我们可以得到(21)D的二进制为(10101)B&…

【C++】STL——容器适配器 stack和queue 深度剖析及模拟实现

文章目录 1. stack的介绍及使用1.1 stack的介绍1.2 stack的使用 2. stack的模拟实现2.1 适配器模式的了解2.2 结构2.3 成员函数 3. queue的介绍及使用3.1 queue的介绍3.2 queue的使用 4. queue的模拟实现5. STL标准库中stack和queue的底层结构6. deque的简单介绍(了解)6.1 dequ…

Linux安装GitLab

一、摘要 公司经常用Gitlab做为代码仓库,这里我也安装下Gitlab,为后面CI/CD集成打下基础,官方安装地址:https://about.gitlab.com/install/#centos-7。不过官网很多人说比较坑,这里就不用官方方法安装了。 二…

在职字节,一个8年软件测试工程师的学习经历

简单的先说一下,坐标北京,15届本科毕业,算上年前在字节跳动的面试,一共有面试了有6家公司(因为不想请假,因此只是每个晚上去其他公司面试,所以面试的公司比较少) 其中成功的有4家&am…

ChatGPT:DevSecOps 落地实践的最后一公里

DevSecOps 背后的三个逻辑 复杂性:让安全从 “幕后” 走向 “台前” 安全并不是一个新鲜的话题,自软件诞生以来,安全就一路伴随,但是近几年安全似乎又到了一个新的 “热度” 与 “高度”。 一些企业、组织都在探讨软件供应链安全…

Vue3迎来升级,助力企业数字化转型

近年来,随着“互联网”的推进与应用普及,数字化转型已经成为企业发展的必备选项。低代码开发平台的迅速普及和广泛应用,也为企业数字化的转型提供了支撑技术。 JNPF快速开发平台深度集成java.net 6 双技术引擎,具备易维护、便部署…

H3C-NE-实验-VLAN的划分

实验拓扑结构图: 1. 配置PC机的IP地址 依次为192.168.1.1/24、192.168.1.2/24、192.168.1.3/24、192.168.1.4/24 注意接口管理要启用,接口状态必须为UP 2. 检测Ping通性 因为4台PC机都在同一个网段,H3C交换机默认端口也是属于Access口&#x…

【c语言】文本文件的读写操作

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c语言系列专栏&#xff1a;c语言之路重点知识整合 &#x…