管程模型与锁

news2024/12/23 11:51:03
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

在如何学习Java并发编程一章中,我画了一幅图:

到目前为止,我们已经学习了Thread、Runnable/Callable,也一起研究了FutureTask的底层原理,还通过手写山寨ThreadPoolExecutor的方式剖析了线程池的设计思想。也就是说,上图左边关于线程的部分已经学习完毕(CompletableFuture属于线程工具,后面介绍)。

相信大家对前面《从线程间通信聊到阻塞队列》一文都还有印象,它看似和多线程没有直接关系,却是承上启下的关键。阻塞队列是线程池生产消费模型的核心,同时它内部的实现又依赖于ReentrantLock,所以阻塞队列可以说是线程和锁之间的“桥梁”。现在线程基本讲完了,该轮到“锁”了。

什么是锁?

学习多线程是为了提高任务执行效率,但多线程又引入了线程安全问题。所谓线程安全问题,可以简单地理解为数据不一致(与预期不一致)。

什么时候可能出现线程安全问题呢?

当同时满足以下三个条件时,才可能引发线程安全问题:

  • 多线程环境
  • 有共享数据
  • 有多条语句操作共享数据/单条语句本身非原子操作(比如i++虽然是单条语句,但并非原子操作)

比如线程A、B同时对int count进行+1操作(初始值假设为1),在一定的概率下两次操作最终结果可能为2,而不是3。

那么加锁为什么能解决这个问题呢?

如果不考虑原子性、内存屏障等晦涩的名词,加锁之所以能保证线程安全,核心就是“互斥”。所谓互斥,就是字面意思上的相排。这里的“互相”是指谁呢?就是多线程之间!

怎么实现多线程之间的互斥呢?

引入“中间人”即可。

注意,这是个非常简单且伟大的思想。在编程世界中,通过引入“中介”最终解决问题的案例不胜枚举,包括但不限于Spring、MQ。在码农之间,甚至流传着一句话:没有什么问题是引入中间层解决不了的。

而JVM锁其实就是线程和线程彼此的“中间人”,多个线程在操作加锁数据前都必须征求“中间人”的同意:

锁在这里扮演的角色其实就是守门员,是唯一的访问入口,所有的线程都要经过它的拷问。在JDK中,锁的实现机制最常见的就是两种,分别是两个派系:

  • synchronized关键字
  • AQS(ReentrantLock)

个人觉得synchronized关键字要比AQS难理解,但AQS的源码比较抽象。这里简要介绍一下Java对象内存结构和synchronized关键字的实现原理。

Java对象内存结构

要了解synchronized关键字,首先要知道Java对象的内存结构。强调一遍,是Java对象的内存结构

它的存在仿佛向我们抛出一个疑问:如果有机会解剖一个Java对象,我们能看到什么?

右上图画了两个对象,只看其中一个即可。我们可以观察到,Java对象内存结构大致分为几块:

  • Mark Word(锁相关)
  • 元数据指针(class pointer,指向当前实例所属的类)
  • 实例数据(instance data,我们平常看到的仅仅是这一块)
  • 对齐(padding,和内存对齐有关)

如果此前没有了解过Java对象的内存结构,你可能会感到吃惊:天呐,我还以为Java对象就只有属性和方法!

是的,我们最熟悉实例数据这一块,而且以为只有这一块。也正是这个观念的限制,导致一部分初学者很难理解synchronized。比如初学者经常会疑惑:

  • 为什么任何对象都可以作为锁?
  • Object对象锁和类锁有什么区别?
  • synchronized修饰的普通方法使用的锁是什么?
  • synchronized修饰的静态方法使用的锁是什么?

这一切的一切,其实都可以在Java对象内存结构中的Mark Word找到答案:

很多同学可能是第一次看到这幅图,会感到有点懵,没关系,我也很头大,都一样的。这里用思维导图帮大家大致梳理下,方便记忆:

Mark Word包含的信息还是蛮多的,但这里我们只需要简单地把它理解为记录锁信息的标记即可。上图展示的是32位虚拟机下的Java对象内存,如果你仔细数一数,会发现全部bit加起来刚好是32位。64位虚拟机下的结构大同小异,就不特别介绍。

Mark Word从有限的32bit中划分出2bit,专门用作锁标志位,通俗地讲就是标记当前锁的状态。

正因为每个Java对象都有Mark Word,而Mark Word能标记锁状态(把自己当做锁),所以Java中任意对象都可以作为synchronized的锁:

synchronized(person){
}
synchronized(student){
}

所谓的this锁就是当前对象,而Class锁就是当前对象所属类的Class对象,本质也是Java对象。synchronized修饰的普通方法底层使用当前对象作为锁,synchronized修饰的静态方法底层使用Class对象作为锁。

但如果要保证多个线程互斥,最基本的条件是它们使用同一把锁:

对同一份数据加两把不同的锁是没有意义的,实际开发时应该注意避免下面的写法:

synchronized(Person.class){
    // 操作count
}

synchronized(person){
    // 操作count
}

或者

public synchronized void method1(){
    // 操作count
}

public static synchronized void method1(){
    // 操作count
}

什么是管程?

粗略回顾完Java的锁之后,该上硬菜了。

首先可以明确的是,管程不是Java专有的概念,也不是某个编程语言独有的概念,而是一种通用的解决方案——一种用于解决并发问题的模型。

引入管程的原因

计算机科班的同学应该对信号量并不陌生,也就是所谓的“信号量与PV操作”,是操作系统层面用来解决同步问题的。但信号量机制的缺点是:

进程自备同步操作,P(S)和V(S)操作大量分散在各个进程中,不易管理,易发生死锁。

1974年和1977年,Hore和Hansen提出了管程。

管程特点:管程封装了同步操作,对进程隐蔽了同步细节,简化了同步功能的调用界面,用户编写并发程序如同编写顺序(串行)程序。

引入管程机制的目的:

  • 把分散在各进程中的临界区集中起来进行管理
  • 防止进程有意或无意的违法同步操作
  • 便于用高级语言来书写程序,也便于程序正确性验证

管程的定义

管程是由自身内部的若干公共变量和所有访问这些公共变量的过程所组成的软件模块。

管程的组成部分

  • 管程内部定义的共享变量
  • 对数据结构(共享变量)进行操作的一组过程(方法)
  • 对管程的数据进行初始化的语句

管程的属性

  • 共享性:管程可被系统范围内的进程互斥访问,属于共享资源
  • 安全性:管程的局部变量只能由管程的过程访问,不允许进程或其它管程直接访问,管程也不能访问非局部于它的变量
  • 互斥性:多个进程对管程的访问是互斥的。任一时刻,管程中只能有一个活跃进程
  • 封装性:管程内的数据结构是私有的,只能在管程内使用,管程内的过程也只能使用管程内的数据结构。进程通过调用管程的过程使用临界资源

从上面对管程的描述来看,你会发现管程还挺符合面向对象设计思想的:定义一个共享变量,对外暴露一组操作。Java之所以选择管程,不知道和这点有无关系。

管程在Java中的实现

管程模型并不是一成不变的,它经历了几次变化:Hoare模型(1974年)、Hasen模型(1976年)以及后来的MESA模型。其中,现在广泛应用的是MESA模型,并且Java 管程的实现参考的也是MESA模型。

synchronized锁

大家在学习synchronized时,如果学得深入些,肯定见过这幅图:

我们都知道,Java的任意对象都可以作为锁,这是怎么做到的呢?

实际上,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后( synchronized(user){...} ),该对象头的Mark Word就被设置指向Monitor对象的指针。也就是说,这个Java对象就关联了一个Monitor对象,而Monitor就是Java的管程模型。所以,真正实现锁机制的并不是Java对象本身,而是它背后的管程。

synchronized的管程模型大致分为EntryList、Owner和WaitSet。抢到锁的线程记录为Owner,抢不到锁的线程进入EntryList等待。那WaitSet是啥?大家想一下,抢到锁但在同步代码块中调用wait()的线程该如何处理?其实就是进入了WaitSet。

总结一下synchronized相关的管程模型(Monitor):

  • 抢到锁的线程是Owner
  • 没抢到锁的进入EntryList
  • 抢到锁,但运行过程中因为条件不满足,调用wait()方法后进入WaitSet,等待notify()/notifyAll()

可以粗浅地理解为,管程模型考虑2个问题:

  • 没抢到锁时如何处理(等待队列)
  • 抢到锁却不满足条件时又如何处理(条件队列)

如果非要再细致一点,再加上一个问题:“如何表示一个线程抢到了锁”。

极客时间王宝令老师在管程:并发编程的万能钥匙中,画了一张图,个人觉得很贴切:

对于synchronize而言:

  • 共享变量大概就是锁对象
  • 条件变量等待队列只有一个WaitSet
  • 入口等待队列就是EntryList
  • 对外暴露的方法就是:wait()、notify()和notifyAll()

由于synchronized是隐式锁,所以并没有提供lock()和unlock()操作,JVM底层帮我们自动完成加解锁操作。

ReentrantLock锁

同样是基于管程模型,但synchronized和ReentrantLock底层实现并不相同。synchronized直接依赖于JVM和操作系统,引入了Monitor对象,而ReentrantLock底层则是AQS。

之前模拟阻塞队列时,我们迭代了多个版本。原先用synchronized实现了一版阻塞队列,但由于出现了“生产者唤醒生产者”的乌龙事件,于是把notify()改成了notifyAll(),后面干脆使用ReentrantLock替代了synchronized,而ReentrantLock.Condition也完美了解决了等待队列的问题。

回顾上面对管程模型的定义:

  • 共享变量(synchronized基于对象内存的markword,ReentrantLock基于AQS的state变量)
  • 对外暴露的操作
  • EntryList和WaitSet

你会发现synchronized的Monitor实现,只有一个等待队列,无论基于什么原因发生等待(wait),统统进入WaitSet。这样一来,当A条件满足要去唤醒当初因为A条件而等待的线程时,不得不把其他条件的等待队列也一并唤醒...简而言之,对等待队列的控制不够精确。

Doug Lea通过抽取封装AQS,实现了自己的EntryList,同时支持不同条件的等待队列Condition。获锁的线程如果当前不满足A条件,可以进入A Condition队列等待,不满足B条件则进入B Condition队列等待。唤醒的过程同样清晰,当A条件满足时,去A Condition队列唤醒即可。

和synchronized一样,ReentrantLock其实也是围绕三个问题设计的:“没抢到锁时如何”、“抢到锁却不符合条件时又如何”、“怎样才算抢到锁”

小结

本文粗略介绍了什么是锁、Java对象的内存结构,然后又引出管程模型。Java对象的内存结构与synchronized的管程实现较为密切,而ReentrantLock则是借助AQS,是Doug Lea参考管程模型编写的抽象同步器。

另外,你会发现从系统底层到JVM层面,阻塞无处不在。Monitor的EntryList、WaitSet,AQS的阻塞队列,甚至BlockingQueue本身,其实都是阻塞模型,只不过一个是微观的,一个是宏观的。但归根到底,都能看到管程模型,所以说管程是并发编程的万能钥匙。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

操作系统大会 openEuler Summit 2023即将召开,亮点不容错过

【12月11日,北京】数字化、智能化浪潮正奔涌而来。操作系统作为数字基础设施的底座,已经成为推动产业数字化、智能化发展的核心力量,为数智未来提供无限可能。12月15-16日,以“崛起数字时代 引领数智未来”为主题的操作系统大会 &…

Python - 搭建 Flask 服务实现图像、视频修复需求

目录 一.引言 二.服务构建 1.主函数 upload_gif 2.文件接收 3.专属目录 4.图像修复 5.gif2mp4 6.mp42gif 7.图像返回 三.服务测试 1.服务启动 2.服务调用 四.总结 一.引言 前面我们介绍了如何使用 Real-ESRGAN 进行图像增强并在原始格式 jpeg、jpg、mp4 的基础上…

ROB的结构与作用

在流水线的提交(Commit)阶段,之所以能够将乱序执行的指令变回程序中指定的顺序状态,主要是通过重排序缓存(Reorder Buffer, ROB)来实现的 ROB本质上是一个FIFO在它当中存储了一条指令的相关信息,例如这条指令的类型、结果、目的寄存器和异常…

QT作业3

完善对话框,点击登录对话框,如果账号和密码匹配,则弹出信息对话框,给出提示”登录成功“,提供一个Ok按钮,用户点击Ok后,关闭登录界面,跳转到其他界面 如果账号和密码不匹配&#xf…

关于照片时间轴修改的方法根据文件名修改拍摄日期、创建日期等信息根据时间戳文件名修改照片信息

由于时光相册倒闭,从云上下载回来的数据有很多是乱序的、没有时间轴 关于照片时间轴修改的方法 根据文件名修改拍摄日期、创建日期等信息 根据时间戳文件名修改照片信息 其中有几种情况 1.文件名带有照片拍摄日期时间 2.文件名是时间戳(微信图片&a…

阿里面试:如何保证RocketMQ消息有序?如何解决RocketMQ消息积压?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题: 如何保证RocketMQ消息有序?如何解决RocketMQ消息…

Docker多平台安装与配置指南

Docker的流行使得它成为开发者和运维人员不可或缺的工具。在本文中,将深入探讨如何在不同平台上安装和配置Docker,旨在为大家提供详尽的指南,确保他们能够顺利地使用这一强大的容器化工具。 Docker基础概念回顾 Docker利用容器技术&#xf…

排序与算法--冒泡排序

1.原理 比较两个相邻的元素,将较大的值交换到右边 2.举例:2 8 7 5 9 4 3 第一趟: 第一次:比较第一个和第二个:2 8 ,不需要交换 第二次:比较 8 7,将8和7进行交换:2 7 …

山海鲸开发者:展现数据可视化在各领域的无限可能

作为一名山海鲸可视化软件的内部开发者,我对这款软件投入了大量的经历以及含有深深的情感。下面,我从这款软件应用场景下手,带大家探秘这款软件的多种可能性以及我们的用心。 首先,从行业角度来看,山海鲸可视化软件可以…

玩法题材创新的跑酷游戏,广告变现不止带来收益 | TopOn变现干货

跑酷游戏是一类永不落伍的游戏。从远古的红白机到现代的PC、手机,经典作品层出不穷,而提起手机端的跑酷游戏,相信大部分玩家脑海里的第一印象便是《神庙逃亡》和《地铁跑酷》这两款经典游戏,在上蹿下跳、左右挪移间躲避障碍&#…

Web自动化测试工具起到哪些重要作用

随着互联网的迅猛发展,Web应用程序已经成为企业不可或缺的一部分。为了确保Web应用的质量和可靠性,Web自动化测试工具变得至关重要。以下是Web自动化测试工具在软件开发生命周期中发挥的作用: 1. 提高测试效率和速度 Web自动化测试工具可以快…

简单实现Spring容器(二) 封装BeanDefinition对象放入Map

阶段2: // 1.编写自己的Spring容器,实现扫描包,得到bean的class对象.2.扫描将 bean 信息封装到 BeanDefinition对象,并放入到Map.思路: 1.将 bean 信息封装到 BeanDefinition对象中,再将其放入到BeanDefinitionMap集合中,集合的结构大概是 key[beanName]–value[beanDefintion…

网贷教父判无期,千家万户哭成狗

作者|翻篇 新熔财经快评: 真是太气人了 e租宝崩盘后 比它更大的雷又来了 “网贷教父”周世平 非法吸收公众存款1090亿 被判了无期 48万多人的血汗钱啊 就这样血本无归了 要知道 当年周世平做p2p 就靠着全额垫付 这颗定心丸 大量的宝妈 上…

作业12.11

1 完善对话框,点击登录对话框,如果账号和密码匹配,则弹出信息对话框,给出提示”登录成功“,提供一个Ok按钮,用户点击Ok后,关闭登录界面,跳转到其他界面 如果账号和密码不匹配&…

前端开发转行做渗透测试容易吗?通过挖漏洞来赚钱靠谱吗?

最近,一个做运维的朋友在学渗透测试。他说,他公司请别人做渗透测试的费用是 2w/人天,一共2周。2周 10w 的收入,好香~ CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享 于是,我也对渗透测…

聊聊跨进程共享内存的内部工作原理

在 Linux 系统的进程虚拟内存中,一个重要的特性就是不同进程的地址空间是隔离的。A 进程的地址 0x4000 和 B 进程的 0x4000 之间没有任何关系。这样确确实实是让各个进程的运行时互相之间的影响降到了最低。某个进程有 bug 也只能自己崩溃,不会影响其它进…

如何将FLV转换为MP3?金狮视频助手告诉你

FLV(Flash video)是一种流行的流媒体视频格式,具有最小的视频文件大小和快速的视频加载速度。但苹果的iOS设备不支持Flash Player插件,因此,要在 iPad 上播放 FLV 视频,您需要将 FLV 转换为 MP4 才能观看。…

力扣编程题算法初阶之双指针算法+代码分析

目录 第一题:复写零 第二题:快乐数: 第三题:盛水最多的容器 第四题:有效三角形的个数 第一题:复写零 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 思路: 上期…

Java CPU使用率高排除方法

CPU使用率高排除方法 查询CPU使用率高的进程查询线程CPU使用率ps命令方式top 命令方式 查询线程堆栈 jstat 查询gc情况 查询CPU使用率高的进程 ps aux --sort-pcpu | head -10通过ps指令看到pid1799程序CPU使用率47.1%,再看看程序的线程使用情况。 查询线程CPU使用…

关于对向量检索研究的一些学习资料整理

官方学习资料 主要是的学习资料是, 官方文档 和官方博客。相关文章还是挺多 挺不错的 他们更新也比较及时。有最新的东西 都会更新出来。es scdn官方博客 这里简单列一些,还有一些其他的,大家自己感兴趣去看。 什么是向量数据库 Elasticse…