Java并发编程实战 04 | 使用WaitNotify时要注意什么?

news2024/9/21 0:49:01

在 Java 中,wait()、notify() 和 notifyAll() 方法在多线程编程中主要用于线程间的协作和同步。理解这些方法的使用特点对于编写稳定的多线程程序至关重要。我们将从以下三个问题入手深入探讨它们的使用:

  1. 为什么必须在 synchronized 代码块中使用 wait() 方法?
  2. 为什么 wait 方法需要在循环中使用?
  3. wait/notify 和 sleep 方法之间的相似点和不同点?

为什么必须在 synchronized 代码块中使用 wait() 方法?

为了找到这个问题的答案,让我们反过来思考:如果我们不要求在synchronized代码块中使用wait方法,会发生什么问题?让我们看看这段代码。

public class QueueDemo {

    Queue<String> buffer = new LinkedList<String>();

    public void save(String data) {
        buffer.add(data);
        
        // // 因为可能有线程在等待 take(),所以通知它们
        notify();  
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

代码中有两个方法:save 和 take。save 方法负责将数据添加到 buffer 中,并调用 notify 方法来唤醒之前等待的线程。

take 方法则检查 buffer 是否为空。如果为空,则进入等待状态;如果不为空,则从 buffer 中获取一个数据项。

这是一个典型的生产者-消费者模式,我将在后续的文章中详细探讨这一模式。

然而,这段代码没有受到 synchronized 关键字的保护,可能会出现以下情况:

  • 首先,消费者线程调用take方法,在take方法中通过buffer.isEmpty()判断buffer是否为空,如果为空,线程要进入等待状态,但是如果线程在调用wait方法之前就被调度器挂起了,此时方法wait还未执行。

  • 与此同时,生产者线程开始运行,并执行 save 方法。它向 buffer 中添加数据,并调用 notify 方法。然而,由于消费者线程的 wait 方法还未执行,因此notify 调用没有任何效果,因为没有任何线程在等待唤醒。

  • 接着,之前被调度器挂起的消费者线程恢复执行,并调用 wait 方法,进入等待状态。错过了先前的唤醒。

虽然消费者在调用 wait 方法之前已经判断了 buffer.isEmpty 的条件,但当 wait 方法实际执行时,之前的判断结果已经过期,因为 buffer 的状态可能已经发生了变化。

这里的“判断-执行”并不是一个原子操作,中途可能被打断,这导致了线程的不安全性。在这种情况下,消费者线程可能由于错过了生产者的 notify 调用而陷入无尽的等待状态。

你可以分别调用这两个方法来模拟一个生产者线程和一个消费者线程:

public class QueueDemo2 {

    Queue<String> buffer = new LinkedList<String>();

    public void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify();  // Since someone may be waiting in take()
    }

    public String take() throws InterruptedException {
        System.out.println("Try to consume a data");
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        QueueDemo2 queueDemo = new QueueDemo2();

        Thread producerThread = new Thread(() -> {
            queueDemo.save("Hello World!");
        });

        Thread consumerThread = new Thread(() -> {
            try {
                System.out.println(queueDemo.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumerThread.start();
        producerThread.start();
    }
}

//输出:
Try to consume a data
Produce a data
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)
    at thread.basic.chapter4.QueueDemo2.save(QueueDemo2.java:13)
    at thread.basic.chapter4.QueueDemo2.lambda$main$0(QueueDemo2.java:28)
    at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at thread.basic.chapter4.QueueDemo2.take(QueueDemo2.java:19)
    at thread.basic.chapter4.QueueDemo2.lambda$main$1(QueueDemo2.java:33)

值得庆幸的是,你根本没有犯错的机会!因为如果 wait 方法和 notify 方法在没有被 synchronized 关键字保护的代码块中执行,Java 会直接抛出 java.lang.IllegalMonitorStateException 异常。

为了解决这个问题,我们需要对代码进行修改:

public class SyncQueueDemo2 {

    Queue<String> buffer = new LinkedList<>();

    public synchronized void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify();  // Since someone may be waiting in take()
    }

    public synchronized String take() throws InterruptedException {
        System.out.println("Try to consume a data");
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        SyncQueueDemo2 queueDemo = new SyncQueueDemo2();

        Thread producerThread = new Thread(() -> {
            queueDemo.save("Hello World!");
        });

        Thread consumerThread = new Thread(() -> {
            try {
                System.out.println(queueDemo.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumerThread.start();
        producerThread.start();
    }
}

//输出:
Produce a data
Try to consume a data
Hello World!

如您所见,程序成功运行,并将“Hello World!”正确打印到控制台。

为什么 wait 方法需要在循环中使用?

当线程调用 wait 方法后,有可能会发生“虚假唤醒”的情况,即线程可能在没有接收到 notify 或 notifyAll 的情况下被意外唤醒,而这是我们不希望看到的。

尽管在实际环境中发生虚假唤醒的概率很小,但程序仍然需要确保在这种情况下的正确性。因此,我们使用 while 循环结构来反复检查等待条件,从而保证线程在被唤醒时,只有在条件满足的情况下才会继续执行。

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

这样,即便是被误唤醒了,也会再次检查 while 中的条件,如果条件不满足,则继续 wait,这样就杜绝了误唤醒的风险。

wait/notify 和 sleep 方法之间的相似点和不同点?

以下是 wait 方法和 sleep 方法之间的相似之处:

  1. 阻塞线程:wait 和 sleep 都会导致当前线程进入阻塞状态。
  2. 响应中断:如果在等待过程中收到中断信号,两者都会响应并抛出 InterruptedException 异常。

但是,它们之间也存在着许多不同之处:

  1. 使用位置不同:wait 方法必须在 synchronized 修饰的代码块或方法中使用,而 sleep 方法没有这个要求,可以在任何地方使用。
  2. 锁处理方式不同:当 wait 方法执行时,线程会主动释放所持有的对象锁;而 sleep 方法不会释放锁,即使它是在同步代码块中执行。
  3. 恢复机制不同:sleep 方法需要指定一个时间,时间到后线程会自动恢复;而 wait 方法(不带参数的情况)表示线程将永久等待,直到被中断或被其他线程唤醒。
  4. 所属类不同:wait 和 notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

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

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

相关文章

字体反爬(一)

网址 http://xxfb.mwr.cn/sq_djdh.html?v1.0 获取相关数据 解决 F12 先找接口吧&#xff0c; 搜索一下表格的数据 直接从表格中复制 复制过来乱码&#xff0c;基本锁定有字体反爬处理 先点进去看看 {"addvnm": "#GkcERlldm4_1725629424756otltag㯼㢴#Fon…

Linux 技巧汇编

10个重要的Linux ps命令实战 显示所有当前进程 根据用户过滤进程 通过cpu和内存使用来过滤进程 通过进程名和PID过滤 根据线程来过滤进程 树形显示进程 显示安全信息 格式化输出root用户&#xff08;真实的或有效的UID&#xff09;创建的进程 使用PS实时监控进程状态 …

泛型列表相关知识

集合 C#中集合是指在system.Collection下的类型&#xff0c;他们大多数是通过实现此命名空间下的接口来实现的。 C#集合是来维护一组对象的数据结构&#xff0c;与数组不同&#xff0c;集合包含更多的功能。如&#xff1a;自动添加元素到指定位置&#xff0c;排序等。 泛型集…

企业级WEB应用服务器---TOMACT

一、WEB技术介绍 1.1 Http和B/S结构 操作系统一般都有子进程系统&#xff0c;使用多进程就可以充分利用硬件资源&#xff0c;提高效率。在前面的学习中我们了解到进程中可以有多个线程&#xff0c;且每一个线程都可以被CPU调度执行&#xff0c;这样就可以让程序并行执行。一台…

深入浅出孪生神经网络,高效训练模型

大家好&#xff0c;在深度学习领域&#xff0c;神经网络几乎能处理各种任务&#xff0c;但通常需要依赖于海量数据来达到最佳效果。然而&#xff0c;对于像面部识别和签名验证这类任务&#xff0c;我们不可能总是有大量的数据可用。由此产生了一种新型的神经网络架构&#xff0…

【自考zt】【数据结构】【22.04】

一、单选 二、填空 三、解答 四、算法阅读 五、算法设计

【Flutter】解决第一次运行项目很慢(gradle需要下载依赖)

配置gradle默认下载路径 默认下C盘谁顶得住 配置环境变量 名称: GRADLE_USER_HOME 值: D:\Develop\gradle 自己创建一个 下边是重点 配置gradle远端下载地址 后边版本号自己换 https://mirrors.cloud.tencent.com/gradle/ https://mirrors.cloud.tencent.com/gradle/gradl…

Matlab 一维层状声子晶体振动传输特性

一维声子晶体的传递矩阵法是一种用于研究声波在一维周期性结构中传播的方法。这种方法基于‌波动方程和周期性边界条件&#xff0c;通过计算声波在不同介质中的传播特性&#xff0c;进而分析声子晶体的带隙结构。传递矩阵法可以有效地预测声波在一维声子晶体中的传播行为&#…

利用AI大语言模型和Langchain开发智能车算法训练知识库(上篇)

今天小李哥将介绍亚马逊云科技的Jupyter Notebook机器学习托管服务Amazon SageMaker上&#xff0c;通过AI大语言模型、向量知识库和LangChain Agent&#xff0c;创建用于AI 智能车模型训练的RAG问答知识库。整个项目的架构图如下&#xff1a; 本系列共分为上下两篇。在上篇内容…

Java中的强引用、软引用、弱引用和虚引用于JVM的垃圾回收机制

参考资料 https://juejin.cn/post/7123853933801373733 在 Java 中&#xff0c;引用类型分为四种&#xff1a;强引用&#xff08;Strong Reference&#xff09;、软引用&#xff08;Soft Reference&#xff09;、弱引用&#xff08;Weak Reference&#xff09;和虚引用&#xf…

5G移动网络运维实验(训)室解决方案

随着第五代移动通信技术&#xff08;5G&#xff09;的快速普及和工业互联网的迅猛发展&#xff0c;全球制造业正面临着前所未有的深刻变革。5G技术凭借其超高的传输速率、极低的延迟以及大规模的连接能力&#xff0c;为工业自动化、智能制造等领域带来了革命性的技术支持。为了…

【免费分享】GIS开发面试题(流程+自我介绍+基础篇+Openlayermapbox)

本篇文章针对GIS应届生就业方向及面试困惑问题进行了收集整理&#xff0c;并列出了关于GIS开发面试中常见的问题&#xff08;含答案&#xff09;。 “ 包括以下内容 前言 简介 面试之前 面试流程 自我介绍-AI 基础篇 1、GIS八股文基础篇 2、Openlayers图形绘制 3、倾…

2-1 opencv实战进阶系列 阈值编辑器

目录 一、不说废话&#xff0c;先上现象 二、前言 三、方法详解 四、贴出完整代码 一、不说废话&#xff0c;先上现象 二、前言 对图像的处理中&#xff0c;设置合适的掩膜、寻找多边形、颜色追踪等方法都需要预先设置好颜色的上阈值和下阈值&#xff0c;来从原图中分割出…

蔚来发布新财报,亏损收窄,营收同比增长98.9%!

KlipC报道&#xff1a;9月5日&#xff0c;蔚来发布2024年二季度财报&#xff0c;财报显示&#xff0c;营收174.5亿元&#xff0c;同比增长98.9%&#xff0c;环比增长76.1%&#xff1b;交付量5.74万台&#xff0c;同比增长143.9%&#xff0c;环比增长90.9%&#xff1b;营收和交付…

Yolov5实现目标检测——调用官方权重进行检测

本文为为&#x1f517;365天深度学习训练营内部文章 原作者&#xff1a;K同学啊 一 安装源码 开源网址&#xff1a;GitHub - ultralytics/yolov5: YOLOv5 &#x1f680; in PyTorch > ONNX > CoreML > TFLite ​ 二 安装所需环境 安装环境依赖包&#xff0c;进入项目…

探索Mem0:下一代人工智能与机器学习内存管理基础设施(二)Mem0+Ollama 部署运行

探索Mem0:下一代人工智能与机器学习内存管理基础设施(二) Mem 0(发音为“mem-zero”)通过智能记忆层增强AI助手和代理,实现个性化的AI交互。Mem 0会记住用户偏好,适应个人需求,并随着时间的推移不断改进,使其成为客户支持聊天机器人,AI助手和自治系统的理想选择。 …

[Mdp] lc198. 打家劫舍(记忆化搜索+dp)

文章目录 1. 题目来源2. 题目解析 1. 题目来源 链接&#xff1a;198. 打家劫舍 前置&#xff1a; [每日一题] 146. 打家劫舍(数组、动态规划、巧妙解法) 2. 题目解析 记忆化搜索可以处理&#xff0c;是自顶向下进行枚举的&#xff0c;属于 递归。 动态规划&#xff0c;属于…

Nuxt3服务端渲染项目简单搭建

目录 1.准备阶段 2.创建项目 3.安装需要的模块 1&#xff09;安装ArcoDesign/ElementPlus 2&#xff09;安装tailwindcss 4.目录结构 5.网站TDK设置 本篇文章相关的官方网站&#xff1a; 1.nuxt3&#xff1a;Nuxt 中文站 - 直观的Web框架 Nuxt3文档 Nuxt 2.vue3&#xf…

fpga系列 HDL:Vivado 安装usb cable驱动

安装usb cable驱动 安装cable_drivers:在data\xicom\cable_drivers\nt64\dlc10_win7目录 安装digilent

数据分析-12-多个时间序列数据的时间戳对齐以及不同的方式补点

参考python时间序列数据的对齐和数据库的分批查询 1 问题场景与分析 1.1 场景 在医院的ICU里,须要持续观察病人的各项生命指标。这些指标的采集频率每每是不一样的(例如有些指标隔几秒采集一个,有些几个小时采集一个,有些一天采集一个),并且有些是按期的,有些是不按期的…