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

【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

目录

〇、先总结一下这三个方法带来的Java线程状态变化

一、obj.wait()

1.1 作用

1.2 使用前需要持有线程共享对象的锁

1.3 使用技巧

二、obj.notify(All)()

1.1 notify() 方法

1.1.1 调用notify()或notifyAll()不会释放线程的锁

1.2 notifyAll() 方法

1.3 使用技巧

三、使用实例

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?


wait()、notify/notifyAll() 方法都是Object的本地final方法,无法被重写。

〇、先总结一下这三个方法带来的Java线程状态变化

当Java线程调用wait()方法后,该线程会进入等待队列,并且会释放占用的锁资源。线程状态会变为WAITING或TIMED_WAITING。该线程不会被挂起到外存,而是在内存中等待被唤醒。线程等待的条件通常是由其他线程调用notify()或notifyAll()方法来唤醒该线程。

当线程被唤醒时,它会重新尝试获取锁资源并从wait()方法返回。线程状态会变为BLOCKED,直到它获得了锁资源为止。如果成功获取锁资源,线程状态会变为RUNNABLE,然后可以继续执行。如果获取锁资源失败,则线程会继续等待,并且状态会维持在BLOCKED或WAITING或TIMED_WAITING状态,直到它再次被唤醒。

需要注意的是,线程在等待期间会消耗一定的资源,因此应该避免过多的线程等待。另外,线程在等待期间不会占用CPU时间片,因此可以减少CPU的利用率,提高系统的性能。

一、obj.wait()

1.1 作用

wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。wait方法还有可以传入等待时长的,可以让线程等待指定的时间后自动被唤醒。调用wait()会使Java线程进入到WAITING状态,调用wait(long time)会使Java线程进入到TIMED_WAITING状态(WAITING和TIMED_WAITING状态就是阻塞状态)

当一个线程调用一个共享变量的wait()方法时,该线程会阻塞(等待)。直到发生以下几种情况才会恢复执行:

  • 其他线程调用了该共享对象的 notify() 方法或者 notifyAll() 方法(继续往下走)
  • 其他线程调用了该线程的 interrupt() 方法,该线程会 InterruptedException 异常返回

等待线程:假设调用的是obj对象的wait()方法,wait的执行线程,也就是被暂停的线程,就称为对象obj上的等待线程。对象的wait方法可能被不同的线程执行,所以同一个对象可能会有多个等待线程。

1.2 使用前需要持有线程共享对象的锁

在使用wait()、notify()和notifyAll()方法方法前,需要先持有锁。如果调用线程共享对象的wait()、notify()和notifyAll()方法的线程没有事先获取该对象的监视器锁,调用线程会抛出IllegalMonitorStateException 异常。当线程调用wait() 之后,就会释放该对象的监视器锁

使用wait()notify()notifyAll()方法方法前,需要先持有锁:

  • 表象:wait、notify(ALL)方法需要调用 monitor 对象
  • 本质:Java的线程通信实质上是共享内存,而不是直接通信

那么,一个线程如何才能获取一个共享变量的监视器锁?

1、执行synchronized 同步代码块,使用该共享变量作为参数。

synchronized(共享变量) {// TODO
}

2、调用该共享变量的同步方法(synchronized 修饰)

synchronized void sum(int a, int b) {// TODO
}

 如下代码示例,线程A与线程B,在线程A中调用共享变量obj的wait()方法,在线程B中进行唤醒notify()。

/*** Object的Wati()方法的使用*/
@Slf4j
public class WaitTest {public static void main(String[] args) {// 定义一个共享变量Object obj = new Object();// 创建线程AThread threadA = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");try {// 获取共享变量的对象锁synchronized(obj){// 线程A 等待log.info("线程" + Thread.currentThread().getName()+"等待");// 调用wait(),线程A阻塞,并且释放掉获取到的obj的对象锁obj.wait();}} catch (InterruptedException e) {e.printStackTrace();}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"A");// 创建线程BThread threadB = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");// 获取共享变量锁synchronized (obj){//  线程B 唤醒或者中断  调用obj的唤醒操作或者使A线程中断的操作都可以将正在阻塞的A线程唤醒log.info("线程" + Thread.currentThread().getName()+"唤醒");obj.notify(); // 唤醒操作// threadA.interrupted(); // 中断操作}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"B");// 启动线程AthreadA.start();try {// 等待200ms,让线程B获取资源,在这200ms期间A就被阻塞了,释放了obj对象锁Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 启动线程BthreadB.start();}
}

执行结果:

线程A开始执行

线程B开始执行

线程B执行结束

线程A执行结束

可以看到主程序线程A启动之后,休眠了200ms让出cup执行权,线程B开始执行后调用notify()方法对阻塞线程A进行唤醒。

故:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

1.3 使用技巧

  • wait()方法一般配合while使用
    • 被唤醒后会重新竞争锁,之后从上次wait位置重新运行
    • while 多次判断,防止在wait这段时间内对象被修改

二、obj.notify(All)()

notify()和notifyAll()方法也是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用notify()和notifyAll()方法。但是线程中共享变量在调用这两个方法前,该线程需要获取到这个共享变量的锁才可以,否则会抛出异常。

1.1 notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait(...) 系列方法后阻塞的线程。

通知线程:调用notify/notifyAll方法时所在的线程叫做通知线程。

1.1.1 调用notify()notifyAll()不会释放线程的锁

当线程调用notify()或notifyAll()方法时,它不会释放掉线程持有的锁。

在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的对象时,它必须先获得该锁的所有权。所以只有获得锁的线程才能调用wait()、notify()和notifyAll()方法。

当线程调用notify()或notifyAll()方法时,它仅仅是唤醒等待在该对象上的一个或多个线程,以便它们可以继续执行。它不会释放线程持有的锁。因此,其他线程仍然无法访问被该锁保护的对象,直到调用notify()或notifyAll()方法的线程释放锁资源。

在多线程编程中,必须小心地管理锁,以避免死锁和竞争条件等问题。通常,为了确保线程安全和避免死锁,必须确保在访问共享资源时只有一个线程持有锁。当然,这也需要合理地使用wait()、notify()和notifyAll()方法来协调线程的执行顺序。

值得注意的是:

  • 一个共享变量上可能会有多个线程在等待,notify()具体唤醒哪个等待的线程是随机的
  • 被唤醒的线程不能马上从wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,等到唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行

1.2 notifyAll() 方法

notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

1.3 使用技巧

尽量让notify / notifyAll()靠近临界区结束的地方。免得等待线程因为没有获得对象的锁,而又进入等待状态。

三、使用实例

比较经典的就是生产者和消费者的例子。

在生产者消费者模型中,推荐使用notifyAll,因为notify唤醒的线程不确定是生产者或消费者。

public class NotifyWaitDemo {// 共享变量队列的最大容量public static final int MAX_SIZE = 1024;// 共享变量public static Queue queue = new Queue();public static void main(String[] args) {// 生产者Thread producer = new Thread(() -> {// 获取共享变量的锁才能调用wait()方法synchronized (queue) {// 一般wait()都配合着while使用,因为线程唤醒后需要不断地轮循来尝试获取锁while (true) {// 当队列满了之后就挂起当前线程(生产者线程)// 并且,释放通过queue的监视器锁,让消费者对象获取到锁,执行消费逻辑if (queue.size() == MAX_SIZE) {try {// 阻塞生产者线程,并且使当前线程释放掉共享变量的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则生成元素,并且通知消费线程queue.add();// 唤醒消费者来消费,建议用notifyAll(),因为notify()无法确定会唤醒哪一个线程queue.notifyAll();}}});// 消费者Thread consumer = new Thread(() -> {// 需要先获取锁synchronized (queue) {while (true) {// 当队列已经空了之后就挂起当前线程(消费者线程)// 并且,释放通过queue的监视器锁,让生产者对象获取到锁,执行生产逻辑if (queue.size() == 0) {try {// 阻塞消费者线程,并释放共享对象的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则消费元素,并且通知生产线程queue.take();queue.notifyAll();}}});// 先执行生产者线程producer.start();try {// 将当前线程睡眠1000ms,让生产者先将队列生产满,然后wait阻塞起来,并且释放持有的锁。为了后续能执行消费者线程Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 执行消费者线程consumer.start();}// 共享变量static class Queue {private int size = 0;public int size() {return this.size;}// 生产操作public void add() {// TODOsize++;System.out.println("执行add 操作,current size: " +  size);}// 消费操作public void take() {// TODOsize--;System.out.println("执行take 操作,current size: " +  size);}}
}

 

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?

由于Thread类继承了Object类,所以Thread也可以调用者三个方法,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。


 相关文章:【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
                  【并发基础】Java中线程的创建和运行以及相关源码分析           
                  【并发基础】线程,进程,协程的详细解释
                 【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解

相关文章:

【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

目录 〇、先总结一下这三个方法带来的Java线程状态变化 一、obj.wait() 1.1 作用 1.2 使用前需要持有线程共享对象的锁 1.3 使用技巧 二、obj.notify(All)() 1.1 notify() 方法 1.1.1 调用notify()或notifyAll()不会释放线程的锁 1.2 notifyAll() 方法 1.3 使用技巧 三、使用实…...

css黏性定位-实现商城的分类滚动的标题吸附

传统的黏性定位是使用js通过计算高度来实现的,当元素滚动到一定位置时吸附在当前位置。下面我们通过css来实现黏性定位功能。 黏性定位 黏性定位目前主流的浏览器已经全部支持,顾名思义,黏性定位具有吸附的效果,其实它是positio…...

@Component和@bean注解在容器中创建实例区别

Component和Bean的区别 在Spring Boot中,Component注解和Bean注解都可以用于创建bean。它们的主要区别在于它们的作用范围和创建方式。 Component注解是一种通用的注解,可以用于标注任何类。被标注的类将被Spring容器自动扫描并创建为一个bean。这个bea…...

不写注释就是垃圾

最近Linux6.2出来了增加了很多新的东西,有看点的是,Linux确实要可以在Apple M1上面运行了,这应该是一个很大的新闻,如果有这么稳定的硬件支持,那对于Linux来说相当于又打下了一大片的江山。其中关于Linux6.2的特性罗列…...

深信服一面

1.C变量存储在哪里,生命周期是怎样的 2.静态成员变量和成员函数的特性,在哪里用过吗 3.new和delete是什么,和malloc和free对比有啥优势 4.new能不能重载,重载new有什么用 5.多态是怎么实现的,有什么优势和目的 6.…...

【C语言】深度理解指针(中)

前言✈上回说到,我们学习了一些与指针相关的数据类型,如指针数组,数组指针,函数指针等等,我们还学习了转移表的基本概念,学会了如何利用转移表来实现一个简易计算器。详情请点击传送门:【C语言】…...

步进电机运动八大算法

引导一种模块化(Module)设计思想,将传统步进电机的控制器(controller)、驱动器(Driver)、运动算法(Arithmetic)三合一。 对比国内外步进电机驱动原理和已有工作,结合各种硬件特性,改进或实现了可实际移植并用于步进电机控制八大算法。本产品…...

如果你持续大量的教坏ChatGPT,它确实会变坏

你输出的很多数据是经过人工标注吗,以确保可以正常对外展示出来,而不是有性别歧视、种族歧视或者其它意识形态为多数人所不认同的内容产生? 作为AI语言模型,我并不直接处理或输出任何数据,我的任务是通过对输入的自然语…...

opencv学习(二)图像阈值和平滑处理

图像阈值ret, dst cv2.threshold(src, thresh, maxval, type)src: 输入图,只能输入单通道图像,通常来说为灰度图dst: 输出图thresh: 阈值maxval: 当像素值超过了阈值(或者小于阈值,…...

【含源码】用python做游戏有多简单好玩

有很多同学问我还有其他什么小游戏吗,游戏是怎么做的,难不难。我就用两篇文章来介绍一下,如何使用Python做游戏。 兔子与灌 俄罗斯方块 休闲五子棋 走迷宫 推箱子 消消乐 超多小游戏玩转不停↓ 更多小游戏可以评论区讨论哦,喜欢…...

C++常用函数

std::sort std::sort 函数用于对数组或容器进行排序&#xff0c;可以按照默认的升序排序或指定比较函数进行排序。 语法如下&#xff1a; template <class RandomAccessIterator> void sort(RandomAccessIterator first, RandomAccessIterator last);template <clas…...

Android Framework基础到深入篇

Android Framework基础到深入篇 KernelSU Android上基于内核的Root方案 Android系统源码下载/编译篇...

【Go进阶训练营】聊一下go的gc原理

背景 正好周末时间&#xff0c;就打算梳理以下自己对go gc的理解。跳出语言层面来说&#xff0c;gc分为两种&#xff0c;一种是手动创建&#xff0c;手动销毁。另一种就是由自动分配自动销毁&#xff0c;前者就是c,c的代表&#xff0c;后者就是java&#xff0c;go。 而整个流程…...

英飞凌Tricore原理及应用介绍05_中断处理之中断路由(IR)模块详解

目录 1.概述1.1相关缩写2 TC3xx中IR特性介绍3.SRN(中断服务请求优先级)3.1 寄存器中的各Bit位讲解3.2 如何改变SRN配置4. 实际应用介绍4.1 如何利用SRC寄存器检查OS中断配置是否正确?1.概述 在Tricore架构中允许有多个中断源包括片上外设及外部中断世间产生的中断请求,以打…...

微搭问答002-移动端上传的文件如何在PC端下载

遇到一个问题&#xff0c;就是上传的图片&#xff0c;在手机上可以下载了&#xff0c;但在电脑上怎么下载到电脑 里&#xff0c;包括上传的文件 点击查看页面就可以吧&#xff0c;在企业工作台里 我做了查看页面&#xff0c;小程序可以&#xff0c;但H5和电脑页面不行 你创建一…...

初识JVM

目录 引言 JVM是什么&#xff1f; JVM和java有什么联系&#xff1f; JDK、JRE、JVM有什么区别 为什么学习JVM&#xff1f; JVM——从内存管理开始 运行时数据区域 分区讲解 堆 方法区 程序计数器 本地技术栈 虚拟机栈 对象的创建 指针碰撞&#xff1a; 空闲列表…...

实践分享:Vue 项目如何迁移小程序

最近我们小组刚经历了将成熟的 HTML5 项目转换成小程序&#xff0c;并在app中运行的操作&#xff01;记录下来分享给各位。 项目&#xff1a;将已有的 Vue 项目转为小程序&#xff0c; 在集成了FinClip SDK 的 App 中运行。 技术&#xff1a;uni-app、FinClip 两个注意事项&…...

JavaScript学习笔记(6.0)

JavaScript类 使用关键字class创建类。 始终添加constructor()方法 class ClassName{constructor(){...} } calss Car{constructor(name,year){this.namename;this.yearyear; } } 创建了一个名为Car的类&#xff0c;并且拥有两个初始属性name和year。 JavaScript类不是对…...

某小公司面试记录

记录一次面试过程&#xff0c;还有一些笔试题&#xff0c;挺简单的&#xff0c;排序&#xff0c;去重&#xff0c;this指向&#xff0c;深浅拷贝&#xff0c;微任务的执行顺序&#xff0c;变量提升等。 ES6数组新增的方法 Array.from&#xff1a; 将两类对象转为真正的数组&am…...

SPI读写SD卡速度有多快?

SD卡是一个嵌入式中非常常用的外设&#xff0c;可以用于存储一些大容量的数据。但用单片机读写SD卡速度一般都有限&#xff08;对于高速SD卡&#xff0c;主要是受限于单片机本身的接口速度&#xff09;&#xff0c;在高速、实时数据存储时可能会有影响。但具体速度可以达到多少…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

P3 QT项目----记事本(3.8)

3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

镜像里切换为普通用户

如果你登录远程虚拟机默认就是 root 用户&#xff0c;但你不希望用 root 权限运行 ns-3&#xff08;这是对的&#xff0c;ns3 工具会拒绝 root&#xff09;&#xff0c;你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案&#xff1a;创建非 roo…...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换

目录 关键点 技术实现1 技术实现2 摘要&#xff1a; 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式&#xff08;自动驾驶、人工驾驶、远程驾驶、主动安全&#xff09;&#xff0c;并通过实时消息推送更新车…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...