19、Java并发 Java wait() 和 notify() 方法

news2025/1/13 10:21:25

大家有没有发现,其实 「 一文秒懂 」 系列讲述的都是多线程并发开发的问题。这个话题太大了,估计没有上百篇文章都解释不清楚。

本文,我们来讲解下 Java 并发中的基础的基础,核心的核心,Java 并发编程中的最基本的机制之一 – 「 线程同步 」

为了方便你理解并发编程中的各种概念和术语,我们首先会来一阵扫盲,讨论一些基本的并发相关术语和方法。接着,我们将开发一个简单的应用程序,并在合格应用程序里处理并发问题,以方便大家理解和巩固 wait() 和 notify()。

Java 中的线程同步 ( Thread Synchronization )

在并发编程中,在多线程环境下,多个线程可能会尝试修改同一资源。如果线程管理不当,这显然会导致一致性问题。

Java 中的哨兵块 ( guarded block )

Java 中,可以用来协调多个线程操作的一个工具是 「 哨兵块 」。这个哨兵块会在恢复执行前检查特定条件。

基于这种哨兵检查的思想,Java 在所有类的基类 Object 中提供了两个方法

方法说明
Object.wait()暂停一个线程
Object.notify()唤醒一个线程

是不是有点难以理解,别担心,看下面这个图,这个图描绘了线程的的生命周期。

虽然从上图中可以看出,有多个方法可以控制一个线程的生命周期,但本章节,我们只讨论 notify() 方法和 wait() 方法

wait() 方法

对照上图,简单的说,当我们调用 wait() 时会强制当前线程等待,直到某个其它线程在同一个对象上调用 notify() 或 notifyAll() 方法。

因此,当前线程必须拥有对象的监视器。根据 Java docs 的说法,这可能发生在

  • 我们已经为给定对象执行了同步实例方法
  • 我们已经在给定对象上执行了 synchronized 块的主体
  • 通过为 Class 类型的对象执行同步静态方法

请注意,一次只有一个活动线程可以拥有对象的监视器。

除了无参数 wait() 方法外,Java 还重载了另一个 wait() 方法

wait() 方法

wait() 方法导致当前线程无限期地等待,直到另一个线程调用此对象的 notify() 或 notifyAll() 方法

wait(long timeout) 方法

使用此方法,我们可以指定一个超时,在此之后将自动唤醒线程。

当然了,我们可以在到达超时之前使用 notify() 或 notifyAll() 提前唤醒线程。

请注意,调用 wait(0) 与调用 wait() 相同

wait(long timeout, int nanos)

这是与wait(long timeout) 提供相同功能的签名,唯一的区别是我们可以提供更高的精度。

该方法计算超时之间的方式为:

总超时时间(以纳秒为单位)= 1_000_000 * 超时 + nanos

notify() 或 notifyAll() 方法

notify() 和 notifyAll() 方法用于唤醒等待访问此对象监视器的线程。

它们以不同的方式通知等待线程。

notify() 方法

对于在此对象的监视器上等待的所有线程(通过使用任何一个重载 wait() 方法 ),notify() 通知将会随机唤醒任何一个线程。

也就是说,我们并不能确切知道唤醒了哪个线程,这取决于实现。

因为notify() 提供了唤醒一个随机线程的机制,因此它可用于实现线程执行类似任务的互斥锁定。

但在大多数情况下,使用 notifyAll() 会是一个更可行的方案。

notifyAll() 方法

notifyAll() 方法用于唤醒正在此对象的监视器上等待的所有线程。唤醒的线程将以常规的方式完成 – 就像任何其他线程一样。

但,有一点要注意的是,对于任意一个线程,但在我们允许其继续执行之前,请始终快速检查继续执行该线程所需的条件。因为在某些情况下线程被唤醒而没有收到通知(这个场景将在后面的例子中讨论 )

发送者 – 接收者同步问题

线程同步的问题,我们已经有了个大概的了解,接下来,我们看一个简单的 Sender-Receiver ( 发送者 – 接收者 ) 应用程序,这个应用程序将利用wait() 和 notify() 方法建立它们之间的同步。

  • 发送者应该向接收者发送数据包
  • 在发送方完成发送之前,接收方无法处理数据包
  • 同样,发送方不得尝试发送另一个数据包,除非接收方已处理过上一个数据包

我们首先创建一个 Data 类,用于包含将从 Sender 发送到 Receiver 的数据包,同时,我们将使用 wait() 和 notifyAll() 来设置它们之间的同步。

public class Data {
    private String packet;

    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;

    public synchronized void send(String packet) {
        while (!transfer) {
            try { 
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
        transfer = false;

        this.packet = packet;
        notifyAll();
    }

    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
        transfer = true;

        notifyAll();
        return packet;
    }
}

范例有点小长,我们一步一步分析下代码

1、 私有属性packet用于表示通过网络传输的数据;
2、 布尔类型的私有属性transfer用于Sender和Receiver之间的同步;

 *  如果此变量为 true,则 Receiver 应等待 Sender 发送消息
 *  如果它是 false ,那么 Sender 应该等待 Receiver 接收消息

3、 Sender使用send()方法将数据发送给Receiver:;

 *  如果 transfer 为 false ,我们将在此线程上调用 wait()
 *  但如果它为 true ,我们需要切换状态,设置我们的消息并调用 notifyAll() 来唤醒其他线程以指定发生了重大事件,然后这些线程它们自己可以自查是否可以继续执行。

4、 同样的,Receiver将使用receive()方法接收数据;

 *  如果 Sender 将传输设置为 false,那么继续,否则将在此线程上调用 wait()
 *  满足条件时,我们切换状态,通知所有等待的线程唤醒并返回 Receiver 的数据包

为什么在 while 循环中包含 wait()

由于notify() 和 notifyAll() 随机唤醒正在此对象监视器上等待的线程,因此满足条件并不总是很重要。有时可能会发生线程被唤醒,但实际上并没有满足条件。

当然了,跟进一步说,我们还可以定义一个检查来避免虚假唤醒 – 线程可以从等待中醒来而不会收到通知。

我们为什么需要同步 send() 和 receive() 方法

我们将这些方法放在 synchronized 方法是为了提供内部锁。

如果调用 wait() 方法的线程不拥有固有锁,则会抛出错误。

现在,是时候创建 Sender 和 Receiver 并在两者上实现 Runnable 接口,以便它们的实例可以由线程执行。

我们先来看看 Sender 将如何工作

public class Sender implements Runnable {
    private Data data;

    // standard constructors

    public void run() {
        String packets[] = {
          "First packet",
          "Second packet",
          "Third packet",
          "Fourth packet",
          "End"
        };

        for (String packet : packets) {
            data.send(packet);

            // Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
    }
}

对于这个 Sender :

  • 我们正在创建一些随机数据包,这些数据包将通过网络以 packet[] 数组的形式发送
  • 对于每个数据包,我们只是调用 send() 而不做其它动作
  • 然后我们用随机时间间隔调用 Thread.sleep() 来模仿繁重的服务器端处理

接下来,我们来看看如何实现 Receiver

public class Receiver implements Runnable {
    private Data load;

    // standard constructors

    public void run() {
        for(String receivedMessage = load.receive();
          !"End".equals(receivedMessage);
          receivedMessage = load.receive()) {

            System.out.println(receivedMessage);

            // ...
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
    }
}

上面这段代码很简单,只是在循环中调用 load.receive() ,直到我们得到最后一个 “End” 数据包。

最后,我们就可以写一个 main() 方法来运行它们了

public static void main(String[] args) {
    Data data = new Data();
    Thread sender = new Thread(new Sender(data));
    Thread receiver = new Thread(new Receiver(data));

    sender.start();
    receiver.start();
}

运行范例,输出结果如下

First packet
Second packet
Third packet
Fourth packet

完美!

我们在这里 – 我们以正确的顺序接收所有数据包,并成功建立了发送方和接收方之间的正确通信。

 

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

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

相关文章

为什么 APISIX Ingress 是比 Ingress NGINX 更好的选择?

作者容鑫,API7.ai 云原生技术工程师,Apache APISIX Committer。 本文将会对比两个比较流行的 Ingress controller 实现,希望能对读者进行 Ingress controller 选型中有所帮助。 Ingress NGINX 是 Kubernetes 社区实现的 Ingress controller&a…

高通量代谢组学四路筛选法,揭秘“神药”二甲双胍延长寿命的机制

百趣代谢组学分享—研究背景 目前据统计中国糖尿病患者人数达9700万以上,数量达到世界第一。这其中2型糖尿病占到了90%以上。二甲双胍是目前治疗2型糖尿病的一线“明星”药物,因其较少出现低血糖和体重增加副作用而受到广大患者和医生的青睐。代谢组学文…

Replicate Brogaard Stock Volatility Decomposition

文章目录IntroductionData and SampleDownload DataClean DataExtract Estimation Unit and Set Global VariablesImplement Brogaard DecompositionEstimate VAR Coefficients, Matrix BBB, ϵt\epsilon_tϵt​, Σe\Sigma_eΣe​, and Σϵ\Sigma_\epsilonΣϵ​Estimate 15-…

常用的前端大屏 适配方案

方案实现方式优点缺点vm vh1.按照设计稿的尺寸,将px按比例计算转为vw和vh1.可以动态计算图表的宽高,字体等,灵活性较高 2.当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况1.每个图表都需要单独做字体、间距、位移的适配&#xf…

【寒假每日一题】AcWing 4509. 归一化处理

目录 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解 三、知识风暴 1、cmath头文件相关函数 2、cout大法 一、题目 1、原题链接 4509. 归一化处理 - AcWing题库 2、题目描述 在机器学习中,对数据进行归一化处理…

【C++】list用法简单模拟实现

文章目录1. list的介绍及使用1.1 list基本概念1.2 list的构造1.3 list的迭代器使用1.4 list 赋值和交换1.5 list 插入和删除1.6 list容量大小操作1.7 list 数据存取2. list的模拟实现这次要模拟实现的类及其成员函数接口总览2.1 结点类的实现2.2 迭代器的模拟实现2.3 反向迭代器…

yolov1 论文精读 - You Only Look Once- Unified, Real-Time Object Detection-统一的实时目标检测

Abstract 我们提出了一种新的目标检测方法- YOLO。以前的目标检测工作重复利用分类器来完成检测任务。相反,我们将目标检测框架看作回归问题,从空间上分割边界框和相关的类别概率。单个神经网络在一次评估中直接从整个图像上预测边界框和类别概率。由于…

PDF体积太大怎么缩小?这两种方法轻松解决

在我们日常处理的文件中,PDF文件的体积已经算是比较小的文件了,但是随着工作时间增加,我们用到的PDF文件也越来越多,而且有些PDF文件的内容非常丰富,文件体积变得更大,这就不利于我们将文件传输给别人&…

人脸检测算法模型MTCNN

MTCNN,Multi-task convolutional neural network(多任务卷积神经网络),将人脸区域检测与人脸关键点检测放在了一起。总体可分为P-Net、R-Net、和O-Net三层网络结构。P-Net是快速生成候选窗口,R-Net进行高精度候选窗口的过滤和选择,O-Net是生成最终边界框和人脸关键点。该…

使用JDK的 keytool 生成JKS,修改查看JKS信息

什么是keytool keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,在JDK 1.4以后的版本中都包含了这一工具,所以不用再上网去找keytool的安装,电脑如果安装有JDK1.4及以上,就可以直接使用。 第一步&…

TOOM舆情分析网络舆情监控平台研究现状

随着网络舆情迅速发展,国内的舆情监测行业也日渐完善,舆情监控平台在企业发展过程中发挥重要作用,但同样也是有问题存在的,接下来TOOM舆情分析网络舆情监控平台研究现状? 一、网络舆情监控平台 网络舆情监控平台是一种能够对网…

maven概述以及简单入门

目录 1、Maven概述 1.1、Maven是什么 1.2 依赖管理 1.3 maven管理资源存放地址 1.4 Maven的作用 2.Maven基础概念 2.1仓库概念 2.坐标概念 1、Maven概述 1.1、Maven是什么 在Javaweb开发中,需要使用大量的jar包,我们手动去导入; 如何…

Mask RCNN网络源码解读(Ⅵ) --- 自定义数据集读取:MS COCOPascal VOC

目录 1.如何在Mask R-CNN中读取有关COCO数据集的内容(my_dataset_coco.py) 1.1 CocoDetection类 1.1.1 初始化方法__init__ 1.1.2 __getitem__方法 1.1.3 parse_targets 2.如何在Mask R-CNN中读取有关Pascal VOC数据集的内容(my_datas…

docker搭建 java web服务

安装 Docker 只需通过以下命令即可安装 Docker 软件: >> rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm >> yum -y install docker-io可使用以下命令,查看 Docker 是否安装成功: …

SpringMvc源码分析(一):启动tomcat服务器,加载DispatcherServlet并将DispatcherServlet纳入tomcat管理

SpringMvc是主流的MVC框架,它是基于Spring提供的web应用框架,该框架遵循servlet规范。该框架的作用是接收Servlet容器(如Tomcat)传递过来的请求并返回响应。SpringMvc的核心就是servlet实例,而这个servlet在spring中就…

IB地理科SL和HL课程的区别

今期我们会谈到IB地理科这一科目的标准级别(StandardLevel,SL)课程和高级级别(HigherLevel,HL)。 两课程的最大区别:试卷数目和题目数量的不同,但两者的教材内容和科目指引(SubjectG…

VTK-不同类型的数据集

前言:本博文主要讲解vtk中不同类型的数据集以及它们之间的关系,如何进行转换等。 目录 vtkImageData vtkRectilinearGrid vtkStructuredGrid vtkUnstructuredPoints vtkPolyData vtkUnstructuredGrid vtkPolyData->vtkImageData vtkPolyData…

Go反射学习

文章目录反射介绍:反射应用点变量-空接口-reflect.Value(Type)类型值方法结构体:反射修改变量值反射操作结构体MethodCall反射介绍: 反射是在运行时,动态的获取变量的各种信息,如变量的类型,类…

Springboot中如何优雅的写好Service层代码

前言《Springboot中如何优雅的写好controller层代码》一不小心进入了全站综合热榜,收到了大家热情的支持,非常感谢大家,同时说明大家都有同样一个诉求,想好好写代码,不想给别人挖坑,争取可以早点下班。今天…

【Spring源码】CommonAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()详解

CommonAnnotationBeanPostProcessor的postProcessMergedBeanDefinition()中一共包含3个方法,上篇文章我们介绍了的第一个方法,它一个父类调用(如下图),其实就是处理PostConstruct和PreDestroy这两个注解的这篇我们继续…