wait、notify、notifyAll 方法的使用注意事项?

news2024/11/16 7:33:52

Java全能学习+面试指南:https://javaxiaobear.cn

我们主要学习 wait/notify/notifyAll 方法的使用注意事项。

我们主要从三个问题入手:

  1. 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
  2. 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
  3. wait/notify 和 sleep 方法的异同?

为什么 wait 必须在 synchronized 保护的同步代码中使用?

首先,我们来看第一个问题,为什么 wait 方法必须在 synchronized 保护的同步代码中使用?

我们先来看看 wait 方法的源码注释是怎么写的。

“wait method should always be used in a loop:

 synchronized (obj) {
     while (condition does not hold)
         obj.wait();
     ... // Perform action appropriate to condition
}

This method should only be called by a thread that is the owner of this object’s monitor.”

英文部分的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。那么设计成这样有什么好处呢?

我们逆向思考这个问题,如果不要求 wait 方法放在 synchronized 保护的同步代码中使用,而是可以随意调用,那么就有可能写出这样的代码。

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
    public void give(String data) {
        buffer.add(data);
        notify();  // Since someone may be waiting in take
    }
    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

在代码中可以看到有两个方法,give 方法负责往 buffer 中添加数据,添加完之后执行 notify 方法来唤醒之前等待的线程,而 take 方法负责检查整个 buffer 是否为空,如果为空就进入等待,如果不为空就取出一个数据,这是典型的生产者消费者的思想。

但是这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:

  1. 首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
  2. 此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
  3. 此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。

虽然刚才消费者判断了 buffer.isEmpty 条件,但真正执行 wait 方法时,之前的 buffer.isEmpty 的结果已经过期了,不再符合最新的场景了,因为这里的“判断-执行”不是一个原子操作,它在中间被打断了,是线程不安全的。

假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 give 方法内的 notify 的唤醒。

我们看到正是因为 wait 方法所在的 take 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作,那么此时整个程序就很容易出错。

我们把代码改写成源码注释所要求的被 synchronized 保护的同步代码块的形式,代码如下。

public void give(String data) {
   synchronized (this) {
      buffer.add(data);
      notify();
  }
}
 
public String take() throws InterruptedException {
   synchronized (this) {
    while (buffer.isEmpty()) {
         wait();
       }
     return buffer.remove();
  }
}

这样就可以确保 notify 方法永远不会在 buffer.isEmpty 和 wait 方法之间被调用,提升了程序的安全性。

另外,wait 方法会释放 monitor 锁,这也要求我们必须首先进入到 synchronized 内持有这把锁。

这里还存在一个“虚假唤醒”(spurious wakeup)的问题,线程可能在既没有被notify/notifyAll,也没有被中断或者超时的情况下被唤醒,这种唤醒是我们不希望看到的。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序依然需要保证在发生虚假唤醒的时候的正确性,所以就需要采用while循环的结构。

while (condition does not hold)
    obj.wait();

这样即便被虚假唤醒了,也会再次检查while里面的条件,如果不满足条件,就会继续wait,也就消除了虚假唤醒的风险。

为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

我们来看第二个问题,为什么 wait/notify/notifyAll 方法被定义在 Object 类中?而 sleep 方法定义在 Thread 类中?主要有两点原因:

  1. 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
  2. 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

wait/notify 和 sleep 方法的异同?

第三个问题是对比 wait/notify 和 sleep 方法的异同,主要对比 wait 和 sleep 方法,我们先说相同点:

  1. 它们都可以让线程阻塞。
  2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

但是它们也有很多的不同点:

  1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  2. 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
  3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  4. wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

以上就是关于 wait/notify 与 sleep 的异同点。

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

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

相关文章

基于灰狼算法的无人机航迹规划-附代码

基于灰狼算法的无人机航迹规划 文章目录 基于灰狼算法的无人机航迹规划1.灰狼搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用灰狼算法来优化无人机航迹规划。 1.灰狼搜索算法 …

Python桌面应用之XX学院水卡报表查询系统(Tkinter+cx_Oracle)

一、功能样式 Python桌面应用之XX学院水卡报表查询系统功能&#xff1a; 连接Oracle数据库&#xff0c;查询XX学院水卡操作总明细报表&#xff0c;汇总数据报表&#xff0c;个人明细报表&#xff0c;进行预览并且支持导出报表 1.总明细报表样式 2.汇总明细样式 3.个人明细…

Proteus仿真--VB上位机程序控制DS1302时钟仿真(Proteus仿真+程序)

本文主要介绍基于51单片机的VB上位机程序控制DS1302时钟仿真设计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 简介 硬件电路主要分为单片机主控模块、DS1302模块、LCD1602液晶显示模块以及串口模块 &#xff08;1&#xff09;单片机主控模块&#xff1a;单片机选…

Unity编辑器扩展 --- AssetPostprocessor资源导入自动设置

unity导入资源的编辑器设置: 防止策划资源乱导入,资源导入需要的格式&#xff0c;统一资源管理 AssetPostprocessor资源导入管线 AssetPostprocessor用于在资源导入时自动做一些设置&#xff0c;比如当导入大量图片时&#xff0c;自动设置图片的类型&#xff0c;大小等。Ass…

volatile-读写屏障插入策略

6.2.4 困难内容 JMM就将内存屏障插入策略分为4种规则 读屏障&#xff1a; 写屏障&#xff1a;

图像语义分割 pytorch复现DeepLab v1图像分割网络详解以及pytorch复现(骨干网络基于VGG16、ResNet50、ResNet101)

图像语义分割 pytorch复现DeepLab v1图像分割网络详解以及pytorch复现&#xff08;骨干网络基于VGG16、ResNet50、ResNet101&#xff09; 背景介绍2、 网络结构详解2.1 LarFOV效果分析 2.2 DeepLab v1-LargeFOV 模型架构2.3 MSc&#xff08;Multi-Scale&#xff0c;多尺度(预测…

Docker Swarm 节点维护

Docker Swarm Mode Docker Swarm 集群搭建 Docker Swarm 节点维护 Docker Service 创建 1.角色转换 Swarm 集群中节点的角色只有 manager 与 worker&#xff0c;所以其角色也只是在 manager 与worker 间的转换。即 worker 升级为 manager&#xff0c;或 manager 降级为 worke…

软件测试 (用例篇)

前言 上一篇博客讲述的是一次基本的测试过程。 在我们开始做了一段时间基础测试&#xff0c;熟悉了业务之后&#xff0c;往往会分配来写测试用例&#xff0c;并且在日常测试中&#xff0c;有时也需要补充测试用例到现有的案例库中。 在这里我们将回答以下问题 1、测试用例的…

Pytorch深度学习快速入门—LeNet简单介绍(附代码)

一、网络模型结构 LeNet是具有代表性的CNN&#xff0c;在1998年被提出&#xff0c;是进行手写数字识别的网络&#xff0c;是其他深度学习网络模型的基础。如下图所示&#xff0c;它具有连狙的卷积层和池化层&#xff0c;最后经全连接层输出结果。 二、各层参数详解 2.1 INPUT层…

上门预约洗鞋小程序开发;

上门洗鞋小程序服务小程序是一款方便用户与服务提供者进行交流和预约的平台&#xff0c;覆盖多个行业&#xff0c;包括家政清洁、洗衣洗鞋&#xff0c;维修服务等&#xff0c;满足用户在生活中各种需求的上门服务。用户可以在小程序中选择服务项目、预约时间&#xff0c;服务人…

JDK的配置及运行过程

文章目录 介绍JDK编译运行过程为什么要配置环境变量配置环境变量的作用 配置JDK验证ps: 介绍JDK 【面试题】JDK、JRE、JVM之间的关系&#xff1f; JDK(Java Development Kit):Java开发工具包&#xff0c;提供给Java程序员使用&#xff0c;包含了JRE&#xff0c;同时还包含了编译…

Mysql如何理解Sql语句?MySql分析器

1. 什么是 MySQL 分析器? MySQL 分析器是 MySQL 数据库系统中的一个关键组件&#xff0c;它负责解析 SQL 查询语句&#xff0c;确定如何执行这些查询&#xff0c;并生成查询执行计划。分析器将 SQL 语句转换为内部数据结构&#xff0c;以便 MySQL 可以理解和执行查询请求。 …

数据集特征预处理

1、什么是特征预处理 1.1、什么是特征预处理 scikit-learn的解释 provides several common utility functions and transformer classes to change raw feature vectors into a representation that is more suitable for the downstream estimators. 翻译过来&#xff1a;通…

VRPTW(MATLAB):斑马优化算法ZOA求解带时间窗的车辆路径问题VRPTW(提供参考文献及MATLAB代码)

一、VRPTW简介 带时间窗的车辆路径问题(Vehicle Routing Problem with Time Windows, VRPTW)是车辆路径问题(VRP)的一种拓展类型。VRPTW一般指具有容量约束的车辆在客户指定的时间内提供配送或取货服务&#xff0c;在物流领域应用广泛&#xff0c;具有重要的实际意义。VRPTW常…

嵌入式系统设计中时钟抖动的基础

嵌入式开发时钟抖动是时钟沿偏离其理想位置的偏差。了解时钟抖动在应用中非常重要&#xff0c;因为它在系统的时序预算中起着关键作用。它有助于嵌入式开发工程师了解系统时序裕度。 随着系统数据速率的提高&#xff0c;时序抖动已成为系统设计中的关键&#xff0c;因为在某些…

[资源推荐]看到一篇关于agent的好文章

链接在此&#xff1a;Chat 向左&#xff0c;Agent 向右 - 李博杰的文章 - 知乎 https://zhuanlan.zhihu.com/p/662704254当时在电脑知乎上看了一半&#xff0c;打开手机微信公众号&#xff0c;就给我推了同样的&#xff0c;这推荐算法&#x1f625;今年关于大模型的想法经历了几…

短视频矩阵系统源码搭建/技术应用开发/源头独立搭建

短视频剪辑矩阵系统开发源码----源头搭建 矩阵系统源码主要有三种框架&#xff1a;Spring、Struts和Hibernate。Spring框架是一个全栈式的Java应用程序开发框架&#xff0c;提供了IOC容器、AOP、事务管理等功能。Struts框架是一个MVC架构的Web应用程序框架&#xff0c;用于将数…

功能基础篇8——图形用户界面

图形用户界面 Graphics User Interface&#xff0c;GUI&#xff0c;图形用户界面 Ubuntu GUI Command Line Interface&#xff0c;CLI&#xff0c;命令行界面 Centos CLI tkinter GUI&#xff0c;Python标准库 from tkinter import ttk, Tkroot Tk() frm ttk.Frame(…

网络工程师最强入职指南

大家好&#xff0c;我是老杨。 秋招即将进入尾声&#xff0c;各位都找到心仪的工作了吗&#xff1f; 今年的春秋招的热度好像不是很高&#xff0c;而且很多网工都是在“全年找工作”的状态里持续着&#xff0c;字里行间无不透露出对行业和自身的焦虑。 毕竟“今年是未来10年…

vue3 elementPlus 表格实现行列拖拽及列检索功能

1、安装vuedraggable npm i -S vuedraggablenext 2、完整代码 <template> <div classcontainer><div class"dragbox"><el-table row-key"id" :data"tableData" :border"true"><el-table-columnv-for"…