条件队列大法好:wait和notify的基本语义

条件队列是我们常用的轻量级同步机制,也被称为“wait+notify”机制。但很多刚刚接触并发的朋友可能会对wait和notify的语义和配合过程感到迷惑。

今天从join()方法的实现切入,重点讲解wait()方法的语义,简略提及notify()与notifyAll()的语义,最后总结二者的配合过程。

本篇的知识点很浅,但牢固掌握很重要。后面会再写一篇文章,介绍wait+nofity的用法,和使用时的一些问题。

基本概念

线程、Thread与Object

在理解“wait+notify”机制时,注意区分线程、Thread与Object的概念,明确三者在wait、 notify、锁竞争等事件中充当的角色:

  • 线程指操作系统中的线程

  • Thread指Java中的线程类

  • Object指Java中的对象

Thread继承自Object,也是一个对象(多态),并从Object类中继承得到了wait()、notify()(还有notifyAll())方法;同时,Thread也被JVM用于映射操作系统中的线程。

wait()

迷惑的join()方法

通过join()方法确认你是否理解了wait+notify机制:

Thread f = new Thread(new Runnable() {@Overidepublic run() {Thread s = new Thread(new Runnable() {@Overidepublic run() {for (int i : 1000000) {sout(i);}}});s.start();sout("************* son thread started *************");s.join();sout("************* son thread died *************");}
});
f.start();

join()方法的语义很简单,可以不严谨的表述为“让父线程等待子线程退出”。现在我们来观察Thread#join()的实现,让你对这个语义产生迷惑:

public final synchronized void join(long millis)
throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}}
}

重点看15-22行。逻辑很简单,一个限时阻塞的经典写法。不过,你可能会产生和我一样的迷惑:

为什么调用子线程的wait()方法,进入等待状态的却是父线程呢?

分析

让我们用前面提到的线程、Thread和Object三个概念来解释这段代码。事件序列如下:

  1. 主线程t0执行1-17行,在Java中创建了Thread实例f,处于NEW状态;同时,f也是一个Object实例

  • 主线程t0执行18行后,操作系统中创建了线程t1,Thread实例f转入RUNNABLE状态(Java中,Thread没有RUN状态,因为线程是否正在执行由JVM之外的调度策略决定)

  • 假设线程t1正在执行,则线程t1执行4-11行,在Java中创建了Thread实例s,处于NEW状态;同时,s也是一个Object实例

  • 线程t1执行12行后,操作系统中创建了线程t2,Thread实例s转入RUNNABLE状态

  • 假设线程t1、t2均正在执行,则线程t1执行12行之后、14行之前,可能线程t1与线程t2同时在向标准输出打印内容(t1执行13行,t2执行7-9行)

  • 线程t1执行14行的过程中,操作系统中的线程t1转入阻塞或等待状态(取决于操作系统的实现),Thread实例f转入TIMED_WAITING状态Thread实例s不受影响,仍处于RUNNABLE状态

  • 线程t2死亡后,被操作系统标记为死亡,Thread实例s转入为TERMINATED状态

  • 线程t1中,Thread实例f发现Thread实例s不再存活,随即转入RUNNABLE状态,操作系统中的线程t1转入运行状态

  • 线程t1从14行s.join()返回,执行15行,打印

  • 最后,线程t1死亡,Thread实例也转入了TERMINATED状态

当然,在事件6(线程t1执行14行的过程中),Thread实例f在TIMED_WAITING状态与RUNNABLE状态之间来回转换,也因此,才能发现Thread实例s不再存活。但可忽略RUNNABLE状态,不影响理解。

上一节提出的问题忽略了线程、Thread与Object的区别。现在,耐心分析过事件序列之后,让我们使用这三个概念,重新表述该问题:

为什么在父线程t1中调用s.join(),进而调用s.wait(),进入等待状态的却是Thread实例f对应的父线程t1,而不是子线程t2呢?

该表述同时也是回答。因为wait()影响的是调用wait()的线程,而不是wait()所属的Object实例。具体说,wait()的语义是“将调用s.wait()的线程t1放入Object实例s的等待集合”。这与s是否同时是Thread实例并无关系——如果s恰好是一个Thread实例,那么其所对应的线程t2可以照常运行,毫无影响。

虽然线程的状态与Thread实例的状态不能一一对应,但用Thread实例的状态代替线程的状态,可以简化条件队列的模型,又不影响核心的正确性。在事件6(线程t1执行14行的过程中)中,各角色的关系如图:

图片

wait+notify的配合过程-2

更容易理解的用法

我们之所以会在join()方法的实现上产生困惑,是因为它以一种难以理解的姿势使用wait+notify机制。

wait+notify机制本质上是一种基于条件队列的同步。JVM为每个对象都内置了监视器,与java.util.concurrent包中的条件队列Condition对应。

条件队列本身很容易理解,但join()方法使用wait()的姿势让人迷惑。它将Thread实例s作为条件队列,共享于父线程t1、子线程t2中——Thread实例s既能够被创建它的Thread实例f访问,也能够被它自己(this)访问。可读性很差,不建议学习。

那么,如何使用wait()才更容易理解呢?可参考Java实现生产者-消费者模型中的“实现二:wait && notify”,使用明确可读的条件队列。简化如下:

public class WaitNotifyModel implements Model {private final Object BUFFER_LOCK = new Object();
...private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {@Overridepublic void consume() throws InterruptedException {synchronized (BUFFER_LOCK) {while (buffer.size() == 0) {BUFFER_LOCK.wait();}Task task = buffer.poll();assert task != null;System.out.println("consume: " + task.no);BUFFER_LOCK.notifyAll();}}}private class ProducerImpl extends AbstractProducer implements Producer, Runnable {@Overridepublic void produce() throws InterruptedException {synchronized (BUFFER_LOCK) {while (buffer.size() == cap) {BUFFER_LOCK.wait();}Task task = new Task(increTaskNo.getAndIncrement());buffer.offer(task);System.out.println("produce: " + task.no);BUFFER_LOCK.notifyAll();}}}
...
}

BUFFER_LOCK即是内置的条件队列。所有生产者线程和消费者线程都共享BUFFER_LOCK,通过BUFFER_LOCK的wait+notify机制实现同步。

  • notify()和notifyAll()接下来讲。

  • 之所以命名为BUFFER_LOCK,是因为同时还要在将BUFFER_LOCK作为内置锁来使用。命名为BUFFER_LOCKBUFFER_COND都是可接受的。

notify()与notifyAll()

可以认为notify与wait是对偶的。s.wait()将当前线程c放入Object实例s的等待集合中,s.notify()随机将一个线程t从s的等待集合中取出来(也可能不是随机的,这取决于操作系统的实现。但很明显JVM的使用者不应该依赖其是否随机)。如果s的等待集合中有多个线程,那么t可能是刚才放入的线程c,也可能是其他线程。

虽然我们通常说“wait+notify”机制,但是使用更多的是notifyAll()而不是notify()。因为notify()只能唤醒一个线程,并且通常是随机的——而被唤醒线程所等待的条件不一定已经被满足(因为多个条件可以使用同一个条件队列),从而会再次进入等待状态;真正满足了条件的线程却因为没被选中而继续等待。这类似于“信号丢失”,可以称为信号劫持

notifyAll()则一次唤醒全部等待在该条件队列上的线程。虽然notifyAll()解决了“信号劫持”的问题,但一次性唤醒全部线程去竞争锁,也大大加剧了无效竞争

关于notify()与notifyAll()的自问自答

如何同时解决信号劫持与无效竞争?

不过,只要保证notify()每次都能叫醒正确的人,就能在解决信号劫持的前提下,避免无效竞争。方法很简单,禁止不同类型的线程共用条件队列

  • 一个条件队列只用来维护一个条件

  • 每个线程被唤醒后执行的操作相同

使用join()方法的过程中,没有任何线程调用notify()或notifyAll(),如何唤醒线程t1?

为了方便理解,前面事件8(线程t1中,Thread实例f发现Thread实例s不再存活)采用了不正确的描述。在事件8之前,线程t1已经处于阻塞状态,从而Thread实例f无法发现s是否不再存活。那么,使用join()方法的过程中,没有任何线程调用notify()或notifyAll(),如何唤醒线程t1?

**在线程t1死亡的时候,JVM会帮忙调用s.notifyAll()**(或非正常死亡时抛出InterruptedException),以唤醒线程t1;t1中做判断,发现s不再存活,便能够正常只是后面的逻辑。

这是必要的。假设JVM不会帮忙(调用s.notifyAll()或抛出InterruptedException),在最坏的情况下,如果线程t1被用户从操作系统中强制杀死,那么在条件队列s上等待的主线程t0将永远阻塞,而不知道此时发生的异常情况。

同时,这种帮助在JVM规范下没有副作用。因为JVM要求用户从wait()方法返回后检查条件是否得到满足。如果用户编写了错误的同步逻辑,使得线程t2正常执行结束后,条件仍不能得到满足,那么虽然JVM的“帮助”使得线程t1提前唤醒,但wait()返回后的检查使线程t1再次进入阻塞状态,符合用户编写的同步逻辑(尽管是错误的)。另一方面,如果没有线程等待条件队列,那么notify也不会做任何事。

wait+notify的配合过程

仍然用Thread实例的状态代替线程的状态。

1. 调用wait()前

调用wait()前,线程t1对应的Thread实例f、t2对应的s都处于RUNNABLE状态:

图片

wait+notify的配合过程-1

2. 调用wait()后,调用notify()前

在线程t1中调用s.wait()后,其他线程调用s.notify()前,t1对应的f转入WAITING状态,进入对象s的等待队列(即,条件队列);s不受影响,仍处于RUNNABLE状态:

图片

wait+notify的配合过程-2

3. 调用notify()后

假设在主线程t0中主动调用s.notify(),那么在此之后,线程t1对应的Thread实例f转入RUNNABLE状态;s仍然不受影响:

图片

原文地址: 条件队列大法好:wait和notify的基本语义

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

天天说微服务,天天开发RESTful API,那你知道RESTful API是什么东东吗?

RESTful API&#xff08;Representational State Transfer&#xff09;是一种基于网络的架构风格&#xff0c;用于设计和构建Web服务。它是一种轻量级的架构&#xff0c;可以通过HTTP协议进行通信&#xff0c;并支持各种数据格式&#xff0c;例如JSON和XML。 在现代的Web应用程…

AI健身教练-引体向上-俯卧撑计数-仰卧起坐姿态估计-康复训练姿态识别-姿态矫正

在AI健身应用中&#xff0c;通过关键点检测技术可以实现对用户动作的精准捕捉和分析&#xff0c;从而进行统计计数和规范性姿态识别。 统计计数&#xff1a;比如在做瑜伽、健身操等运动时&#xff0c;系统可以通过对人体关键点&#xff08;如手部、脚部、关节等&#xff09;的…

克莱姆森大学学术校园生活体育研究影响和认可杜克大学学术优势校园生活和设施研究和创新全球影响结论兄弟会和姐妹会起源发展未来发展趋势STEM

目录 克莱姆森大学 学术 校园生活 体育 研究 影响和认可 杜克大学 学术优势 校园生活和设施 研究和创新 全球影响 结论 兄弟会和姐妹会 起源 发展 未来发展趋势 STEM专业实习期 克莱姆森大学 克莱姆森大学&#xff08;Clemson University&#xff09;是位于美…

如何将Git拉取项目后,将SSH验证方式修改为HTTPS?

首先在打开项目所在位置的Git BashGUI 查找当前的远程仓库URL&#xff1a; 打开终端或命令提示符&#xff0c;导航到你的项目目录&#xff0c;并使用以下命令查看当前配置的远程仓库URL&#xff1a; git remote -v这会显示如下格式的输出&#xff1a; origin gitgithub.com:用…

2、FreeRTOS之队列管理

xQueueReceive() 用于从队列中接收 ( 读取&#xff09;数据单元。接收到的单元同时会从队列 中删除。 xQueuePeek() 也是从从队列中接收数据单元&#xff0c;不同的是并不从队列中删出接收到 的单元。 uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。 写队列任…

perl 用 XML::DOM 解析 Freeplane.mm文件,生成测试用例.csv文件

Perl 官网 www.cpan.org 从 https://strawberryperl.com/ 下载网速太慢了 建议从 https://download.csdn.net/download/qq_36286161/87892419 下载 strawberry-perl-5.32.1.1-64bit.zip 约105MB 解压后安装.msi&#xff0c;装完后有520MB&#xff0c;建议安装在D:盘。 运行 …

【目标检测】原始的 YOLOv1 网络结构(GoogLeNet 作为 backbone 的实现)

现在看网上的很多 YOLOv1 的代码实现&#xff0c;基本都是使用新的 backbone&#xff0c;例如 ResNet 或者 VGG 来实现的&#xff0c;因为这些后面的通用的 backbone 可能比较方便的获得预训练模型&#xff0c;不需要从头开始训练。 但是我就是想看一下&#xff0c;一开始 YOL…

力扣hot100:416.分割等和子集(组合/动态规划/STL问题)

组合数问题 我们思考一下&#xff0c;如果要把数组分割成两个子集&#xff0c;并且两个子集的元素和相等&#xff0c;是否等价于在数组中寻找若干个数使之和等于所有数的一半&#xff1f;是的&#xff01; 因此我们可以想到&#xff0c;两种方式&#xff1a; ①回溯的方式找到t…

批量查询快递不再难,前缀单号助你轻松搞定!

在快递业务日益繁忙的当下&#xff0c;批量查询快递单号成为了许多人的迫切需求。如何能够快速、准确地找到所需的快递单号呢&#xff1f;其实&#xff0c;利用前缀单号进行批量查询是一个高效且实用的方法。下面&#xff0c;就让我们一起了解如何利用前缀单号轻松查找快递单号…

Delphi7应用教程学习1.3【练习题目】:文本及悬停文字的显示

这个例子主要用到了btn的Hint 属性&#xff0c;Hint是提示的意思。 还有Delphi7还是很好用的&#xff0c;改变了的属性是粗体&#xff0c;默认没有改变的属性为细体。

力扣L10--- 3. 无重复字符的最长子串--2024年3月14日

1.题目 2.知识点 注1&#xff1a;containsKey 是 Java 中 HashMap 类的一个方法&#xff0c;用于检查哈希表中是否包含指定的键。 注2&#xff1a;在哈希表&#xff08;HashMap)中&#xff0c;每个键对应着唯一的值&#xff0c;因此键不能重复&#xff0c;但值可以重复。 (1)创…

Linux基础命令[19]-id

文章目录 1. id 命令说明2. id 命令语法3. id 命令示例3.1 不加参数3.2 -u/-g/-G&#xff08;用户、组、所属组&#xff09;3.3 -gr/-Gr/-ur&#xff08;有效ID&#xff09; 4. 总结 1. id 命令说明 id&#xff1a;显示真实有效的用户ID(UID)和组ID(GID)&#xff0c;十分方便&…