Day828.多线程原语:管程 -Java 并发编程实战

news2024/12/26 22:03:32

多线程原语:管程

Hi,我是阿昌,今天学习记录的是关于多线程原语:管程的内容。

并发编程这个技术领域已经发展了半个世纪了,相关的理论和技术纷繁复杂。

那有没有一种核心技术可以很方便地解决的并发问题呢?

这个问题如果让我选择,一定会选择管程技术。

Java 语言在 1.5 之前,提供的唯一的并发原语就是管程,而且 1.5 之后提供的 SDK 并发包,也是以管程技术为基础的。

除此之外,C/C++、C# 等高级语言也都支持管程。

可以这么说,管程就是一把解决并发问题的万能钥匙


一、什么是管程

不知道是否曾思考过这个问题:

为什么 Java 在 1.5 之前仅仅提供了 synchronized 关键字及 wait()、notify()、notifyAll() 这三个看似从天而降的方法?

在刚接触 Java 的时候,以为它会提供信号量这种编程原语,因为操作系统原理课程告诉我,用信号量能解决所有并发问题,结果发现不是。

后来我找到了原因:Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分

而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。

管程,对应的英文是Monitor,很多 Java 领域的同学都喜欢将其翻译成“监视器”,这是直译。

操作系统领域一般都翻译成“管程”,这个是意译,而自己也更倾向于使用“管程”。

所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。

翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。那管程是怎么管的呢?


二、MESA 模型

在管程的发展史上,先后出现过三种不同的管程模型,分别是:

  • Hasen 模型
  • Hoare 模型
  • MESA 模型

其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。所以重点介绍一下 MESA 模型。

在并发编程领域,有两大核心问题:

  • 一个是互斥,即同一时刻只允许一个线程访问共享资源;
  • 另一个是同步,即线程之间如何通信、协作。

这两大问题,管程都是能够解决的。先来看看管程是如何解决互斥问题的。

管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来

假如要实现一个线程安全的阻塞队列,一个最直观的想法就是:

将线程不安全的队列封装起来,对外提供线程安全的操作方法,例如入队操作和出队操作。

利用管程,可以快速实现这个直观的想法。

在下图中,管程 X 将共享变量 queue 这个线程不安全的队列和相关的操作入队操作 enq()、出队操作 deq() 都封装起来了;

线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。

不知你有没有发现,管程模型和面向对象高度契合的。

互斥锁用法,其背后的模型其实就是它。

在这里插入图片描述
那管程如何解决线程间的同步问题呢?

这个就比较复杂了,不过可以借鉴一下曾经提到过的就医流程,它可以帮助快速地理解这个问题。

为进一步便于你理解,在下面,展示了一幅 MESA 管程模型示意图,它详细描述了 MESA 模型的主要组成部分

在管程模型里,共享变量对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。

框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。这个过程类似就医流程的分诊,只允许一个患者就诊,其他患者都在门口等待。

管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列,如下图,条件变量 A 和条件变量 B 分别都有自己的等待队列。

在这里插入图片描述

那条件变量和条件变量等待队列的作用是什么呢?其实就是解决线程同步问题

可以结合上面提到的阻塞队列的例子加深一下理解(阻塞队列的例子,是用管程来实现线程安全的阻塞队列,这个阻塞队列和管程内部的等待队列没有关系,本文中一定要注意阻塞队列和等待队列是不同的)。

假设有个线程 T1 执行阻塞队列的出队操作,执行出队操作,需要注意有个前提条件,就是阻塞队列不能是空的(空队列只能出 Null 值,是不允许的),阻塞队列不空这个前提条件对应的就是管程里的条件变量。

如果线程 T1 进入管程后恰好发现阻塞队列是空的,那怎么办呢?

等待啊,去哪里等呢?就去条件变量对应的等待队列里面等。

此时线程 T1 就去“队列不空”这个条件变量的等待队列中等待。这个过程类似于大夫发现你要去验个血,于是给你开了个验血的单子,你呢就去验血的队伍里排队。线程 T1 进入条件变量的等待队列后,是允许其他线程进入管程的。

这和去验血的时候,医生可以给其他患者诊治,道理都是一样的。

再假设之后另外一个线程 T2 执行阻塞队列的入队操作,入队操作执行成功之后,“阻塞队列不空”这个条件对于线程 T1 来说已经满足了,此时线程 T2 要通知 T1,告诉它需要的条件已经满足了。

当线程 T1 得到通知后,会从等待队列里面出来,但是出来之后不是马上执行,而是重新进入到入口等待队列里面。这个过程类似你验血完,回来找大夫,需要重新分诊。

条件变量及其等待队列我们讲清楚了,下面再说说 wait()、notify()、notifyAll() 这三个操作。前面提到线程 T1 发现“阻塞队列不空”这个条件不满足,需要进到对应的等待队列里等待。这个过程就是通过调用 wait() 来实现的。

如果用对象 A 代表“阻塞队列不空”这个条件,那么线程 T1 需要调用 A.wait()。同理当“阻塞队列不空”这个条件满足时,线程 T2 需要调用 A.notify() 来通知 A 等待队列中的一个线程,此时这个等待队列里面只有线程 T1。

至于 notifyAll() 这个方法,它可以通知等待队列中的所有线程。

下面的代码用管程实现了一个线程安全的阻塞队列(再次强调:这个阻塞队列和管程内部的等待队列没关系,示例代码只是用管程来实现阻塞队列,而不是解释管程内部等待队列的实现原理)。

阻塞队列有两个操作分别是入队和出队,这两个方法都是先获取互斥锁,类比管程模型中的入口。

  1. 对于阻塞队列的入队操作,如果阻塞队列已满,就需要等待直到阻塞队列不满,所以这里用了notFull.await();。
  2. 对于阻塞出队操作,如果阻塞队列为空,就需要等待直到阻塞队列不空,所以就用了notEmpty.await();。
  3. 如果入队成功,那么阻塞队列就不空了,就需要通知条件变量:阻塞队列不空notEmpty对应的等待队列。
  4. 如果出队成功,那就阻塞队列就不满了,就需要通知条件变量:阻塞队列不满notFull对应的等待队列。
public class BlockedQueue<T>{
  final Lock lock =
    new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull =
    lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =
    lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满 
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

在这段示例代码中,用了 Java 并发包里面的 Lock 和 Condition,这个例子只是先让你明白条件变量及其等待队列是怎么回事。

需要注意的是:

  • await() 和前面提到的 wait() 语义是一样的;

  • signal() 和前面我们提到的 notify() 语义是一样的。


三、wait() 的正确姿势

但是有一点,需要再次提醒,对于 MESA 管程来说,有一个编程范式,就是需要在一个 while 循环里面调用 wait()。

这个是 MESA 管程特有的。



while(条件不满足) {
  wait();
}

Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后,如何通知相关线程。管程要求同一时刻只允许一个线程执行,那当线程 T2 的操作使线程 T1 等待的条件满足时,T1 和 T2 究竟谁可以执行呢?

  1. Hasen 模型里面,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。
  2. Hoare 模型里面,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
  3. MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。

四、notify() 何时可以使用

还有一个需要注意的地方,就是 notify() 和 notifyAll() 的使用,前面章节,曾经介绍过,除非经过深思熟虑,否则尽量使用 notifyAll()。

那什么时候可以使用 notify() 呢?

需要满足以下三个条件:

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需要唤醒一个线程。

比如上面阻塞队列的例子中,对于“阻塞队列不满”这个条件变量,其等待线程都是在等待“阻塞队列不满”这个条件,反映在代码里就是下面这 3 行代码。

对所有等待线程来说,都是执行这 3 行代码,重点是 while 里面的等待条件是完全相同的。



while (阻塞队列已满){
  // 等待队列不满
  notFull.await();
}

所有等待线程被唤醒后执行的操作也是相同的,都是下面这几行:



// 省略入队操作...
// 入队后,通知可出队
notEmpty.signal();

同时也满足第 3 条,只需要唤醒一个线程。所以上面阻塞队列的代码,使用 signal() 是可以的。


五、总结

管程是一个解决并发问题的模型,可以参考医院就医的流程来加深理解。

理解这个模型的重点在于理解条件变量及其等待队列的工作原理。

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。

具体如下图所示。

在这里插入图片描述

Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;

Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。

并发编程里两大核心问题——互斥同步,都可以由管程来帮你解决。

学好管程,理论上所有的并发问题都可以解决,并且很多并发工具类底层都是管程实现的,所以学好管程,就是相当于掌握了一把并发编程的万能钥匙。


  1. 管程是一种概念,任何语言都可以通用。
  2. 在java中,每个加锁的对象都绑定着一个管程(监视器)
  3. 线程访问加锁对象,就是去拥有一个监视器的过程。如一个病人去门诊室看医生,医生是共享资源,门锁锁定医生,病人去看医生,就是访问医生这个共享资源,门诊室其实是监视器(管程)。
  4. 所有线程访问共享资源,都需要先拥有监视器。就像所有病人看病都需要先拥有进入门诊室的资格。
  5. 监视器至少有两个等待队列。一个是进入监视器的等待队列一个是条件变量对应的等待队列。后者可以有多个。就像一个病人进入门诊室诊断后,需要去验血,那么它需要去抽血室排队等待。另外一个病人心脏不舒服,需要去拍胸片,去拍摄室等待。
  6. 监视器要求的条件满足后,位于条件变量下等待的线程需要重新在门诊室门外排队,等待进入监视器。就像抽血的那位,抽完后,拿到了化验单,然后,重新回到门诊室等待,然后进入看病,然后退出,医生通知下一位进入。

总结起来就是,管程就是一个对象监视器。任何线程想要访问该资源,就要排队进入监控范围。进入之后,接受检查,不符合条件,则要继续等待,直到被通知,然后继续进入监视器。


wait() 方法,在 Hasen 模型和 Hoare 模型里面,都是没有参数的,而在 MESA 模型里面,增加了超时参数,你觉得这个参数有必要吗?

有必要;

  • hasen 是执行完,再去唤醒另外一个线程。能够保证线程的执行。

  • hoare,是中断当前线程,唤醒另外一个线程,执行玩再去唤醒,也能够保证完成。

  • mesa是进入等待队列,不一定有机会能够执行。


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

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

相关文章

JAVA毕业设计——基于SpringBoot的健身房管理系统(源代码+数据库)

github代码地址 https://github.com/ynwynw/gym-public 毕业设计所有选题地址 https://github.com/ynwynw/allProject 基于SpringBoot的健身房管理系统(源代码数据库) 一、系统介绍 系统层次结构图如下&#xff1a; 管理员登录模块会员管理模块教练管理模块课程管理模块器材…

基于Java+SQL Server2000实现(界面)学生选课管理系统【100010025】

学生选课管理系统 二、需求描述 本系统是一个单机版的小型的学生选课管理系统&#xff0c;在本系统中&#xff0c;开发了一个面向管理员、学生和教师这三个对象的教务平台&#xff0c;对学生提供的服务有登录、选课、、修改登录密码、和查询成绩这几个服务&#xff0c;所选课…

第二季5:配置视频捕获(step3:VI部分)

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 前言 本文将详细介绍博文第二季3&#xff1a;sample_venc.c的整体分析提及的“配置视频捕获”。 该部分主要涉及以下步骤&#xff1a; 5、配置MIPI6、初始化ISP7、运行ISP线程8、配置开启VI 设备捕…

Mybatis 参数处理器注入配置解析流程

参数处理器的作用 Mybatis作为一个ORM框架&#xff0c; 其最原始的本质就是JDBC&#xff0c;对于JDBC的使用步骤中有2步和参数处理器有关&#xff0c; 就是给预处理器PreparedStatement 设置参数以及通过结果集获取字段值。 这两个步骤在Mybatis中已经成为框架底层逻辑流程&am…

【K8S系列】第十一讲:包管理神器-Helm

目录 序言 1.背景介绍 1.1 k8s 简单介绍 1.2 k8s部署挑战 2.Helm 2.1 Helm介绍 2.1 使用优势&#xff1a; 3.Helm模块 3.1 Helm 3.1.1 安装Helm 3.2 Chart 3.2.1 Chart 基本介绍 3.2.2 Chart目录结构 3.3 Repoistory 3.4 Config 3.5 Release 4.投票 序言 当…

Windows及Linux系统查找某个端口和文件被占用

概述 开发中很常见的问题&#xff0c;每次遇到这个问题&#xff0c;都是去Google搜索&#xff0c;不一定能搜到满意的答案&#xff0c;有点耗时&#xff0c;故记录一下&#xff0c;得到本文。 端口被占用&#xff0c;导致IDEA启动应用失败。又或者某个文件被某个未知的应用使…

c#入门-抽象类

抽象类 有些类存在的意义就是为了让别的类继承。自己本身不应该具有实例。例如 class 单位 {public int 生命; } class 建筑 : 单位 {public int 护甲; } class 英雄 : 单位 {public int 护盾; } class 小兵 : 单位 {public int 经验; }可以使用abstract把他声明为抽象类。 抽…

Linux查找find命令全面剖析

Linux查找find命令全面剖析 1. 文件查找 在文件系统上查找符合条件的文件 1.1 简述 locate 命令 非实时查找(数据库查找) 依赖于事先构建的索引&#xff0c;索引的构建是在系统较为空闲时自动进行(周期性任务) 手动更新数据库(updatedb)&#xff0c;索引构建过程需要遍历整…

【物理应用】基于Matlab模拟13自由度摩托车模型

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Python使用Selenium WebDriver的入门介绍及安装教程

Selenium WebDriver 入门一、什么是Selenium WebDriver二、安装Selenium WebDriver2.1 安装selenium类库2.2 安装浏览器驱动2.3 配置环境变量三、编写第一个Selenium脚本一、什么是Selenium WebDriver WebDriver 以本地化方式驱动浏览器&#xff0c;就像用户在本地或使用 Sele…

[附源码]Nodejs计算机毕业设计基于Java网上玩具商店Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

R语言基于协方差的SEM结构方程模型中的拟合指数

在实践中&#xff0c; 因子负载较低&#xff08;或测量质量较差&#xff09;的模型的拟合指数要好于因子负载较高的模型。 最近我们被客户要求撰写关于SEM的研究报告&#xff0c;包括一些图形和统计输出。例如&#xff0c;如果两个模型具有相同的错误指定级别&#xff0c;并且…

漫游Linux块IO

前言 在计算机的世界里&#xff0c;我们可以将业务进行抽象简化为两种场景——计算密集型和IO密集型。这两种场景下的表现&#xff0c;决定这一个计算机系统的能力。数据库作为一个典型的基础软件&#xff0c;它的所有业务逻辑同样可以抽象为这两种场景的混合。因此&#xff0…

云原生时代数据库运维体系演进

作者&#xff1a;vivo 互联网服务器团队- Deng Song 本文根据邓松老师在“2022 vivo开发者大会"现场演讲内容整理而成。 数据库运维面临着大规模数据库实例难以有效运维、数据库难以做好资源弹性伸缩以及个人隐私数据安全难以保障这三个方面的挑战。对此&#xff0c;vivo给…

redis之事务分析

写在前面 本文一起看下redis提供的事务功能。 1&#xff1a;事务的ACID A(Atomic)原子性&#xff0c;C&#xff08;Consitency&#xff09;一致性&#xff0c;I&#xff08;Isolation&#xff09;隔离性&#xff0c;D&#xff08;Durability&#xff09;持久性&#xff0c;其…

kubernetes学习之路--BadPods(Part1)

摘要&#xff1a;对Pod配置进行实战学习&#xff0c;以BadPods项目为例学习危险配置。 目录 一.BadPods介绍及使用 二.BadPods配置学习 2.1 less1--Everything allowed 基本操作学习 2.2 less1--Everything allowed 渗透学习 一.BadPods介绍及使用 项目地址&#xff1a;h…

西门子KTP1200触摸屏右上角出现黄色感叹号_报警指示器的组态与应用

西门子KTP1200触摸屏右上角出现黄色感叹号_报警指示器的组态与应用 设备运行时产生报警时通常会在画面右上角有个指示器在闪烁提示报警产生。 本次和大家分享报警指示器的组态和具体使用方法。 报警指示器的组态。 报警指示器使用警告三角来表示报警处于未决状态或要求确认。如…

数字验证学习笔记——SystemVerilog芯片验证15 ——随机约束和分布

一、随机和约束 1.1 随机 定向测试能找到你认为可能存在的缺陷&#xff0c;而随机测试可以找到你没有想到的缺陷。随机测试相对于定向测试可以减少相当多的代码量&#xff0c;而产生的激励较定向测试也更多样。 1.2 约束 我们想要的随机自由是一种合法的随机&#xff0c;需…

JAVA毕业设计——基于Springboot+vue的房屋租赁系统(源代码+数据库)

github代码地址 https://github.com/ynwynw/houserent2-public 毕业设计所有选题地址 https://github.com/ynwynw/allProject 基于Springboot的房屋租赁系统(源代码数据库) 一、系统介绍 本项目分为管理员、经纪人、维修员、普通用户四种角色 管理员角色包含以下功能&#…

C++——AVL树

目录 AVL 树 Insert 控制平衡因子 AVL树的旋转 AVL树验证 AVL树的性能 错误排查技巧 AVL 树 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。因此&…