Java内存模型(JMM)是基于多线程的吗

Java内存模型(JMM)是基于多线程的吗

这个问题按我的思路转换了下,其实就是在问:为什么需要Java内存模型

总结起来可以由几个角度来看待「可见性」、「有序性」和「原子性」


面试官今天想跟你聊聊Java内存模型,这块你了解过吗?

候选者:嗯,我简单说下我的理解吧。那我就从为什么要有Java内存模型开始讲起吧

面试官:开始你的表演吧。

候选者:那我先说下背景吧

候选者:1. 现有计算机往往是多核的,每个核心下会有高速缓存。高速缓存的诞生是由于「CPU与内存(主存)的速度存在差异」,L1和L2缓存一般是「每个核心独占」一份的。

候选者:2. 为了让CPU提高运算效率,处理器可能会对输入的代码进行「乱序执行」,也就是所谓的「指令重排序」

候选者:3. 一次对数值的修改操作往往是非原子性的(比如i++实际上在计算机执行时就会分成多个指令)

候选者:在永远单线程下,上面所讲的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器/runtime/处理器都必须遵守as-if-serial语义,遵守as-if-serial意味着它们不会对「数据依赖关系的操作」做重排序。

img

候选者:CPU为了效率,有了高速缓存、有了指令重排序等等,整块架构都变得复杂了。我们写的程序肯定也想要「充分」利用CPU的资源啊!于是乎,我们使用起了多线程

候选者:多线程在意味着并发,并发就意味着我们需要考虑线程安全问题

候选者:1. 缓存数据不一致:多个线程同时修改「共享变量」,CPU核心下的高速缓存是「不共享」的,那多个cache与内存之间的数据同步该怎么做?

候选者:2. CPU指令重排序在多线程下会导致代码在非预期下执行,最终会导致结果存在错误的情况。

img

候选者:针对于「缓存不一致」问题,CPU也有其解决办法,常被大家所认识的有两种:

候选者:1.使用「总线锁」:某个核心在修改数据的过程中,其他核心均无法修改内存中的数据。(类似于独占内存的概念,只要有CPU在修改,那别的CPU就得等待当前CPU释放)

候选者:2.缓存一致性协议(MESI协议,其实协议有很多,只是举个大家都可能见过的)。MESI拆开英文是(Modified (修改状态)、Exclusive (独占状态)、Share(共享状态)、Invalid(无效状态))

候选者:缓存一致性协议我认为可以理解为「缓存锁」,它针对的是「缓存行」(Cache line) 进行”加锁”,所谓「缓存行」其实就是 高速缓存 存储的最小单位。

img

面试官:嗯…

候选者:MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的「对象状态」(是修改、还是共享、还是独占、还是无效)。

候选者:如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取

候选者:如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改

候选者:如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),会当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)

候选者:如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。

img

候选者:其实MESI协议做的就是判断「对象状态」,根据「对象状态」做不同的策略。关键就在于某个CPU在对数据进行修改时,需要「同步」通知其他CPU,表示这个数据被我修改了,你们不能用了。

候选者:比较于「总线锁」,MESI协议的”锁粒度”更小了,性能那肯定会更高咯

面试官但据我了解,CPU还有优化,你还知道吗?

候选者:嗯,还是了解那么一点点的。

候选者:从前面讲到的,可以发现的是:当CPU修改数据时,需要「同步」告诉其他的CPU,等待其他CPU响应接收到invalid(无效)后,它才能将高速缓存数据写到主存。

候选者:同步,意味着等待,等待意味着什么都干不了。CPU肯定不乐意啊,所以又优化了一把。

候选者:优化思路就是从「同步」变成「异步」。

候选者:在修改时会「同步」告诉其他CPU,而现在则把最新修改的值写到「[store buffer](https://www.zhihu.com/search?q=store buffer&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={“sourceType”%3A"answer"%2C"sourceId"%3A2215772844})」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。

候选者:其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」

img

候选者:而异步又会带来新问题:那我现在CPU修改完A值,写到「store buffer」了,CPU就可以干其他事了。那如果该CPU又接收指令需要修改A值,但上一次修改的值还在「store buffer」中呢,没修改至高速缓存呢。

候选者:所以CPU在读取的时候,需要去「store buffer」看看存不存在,存在则直接取,不存在才读主存的数据。【Store Forwarding】

候选者:好了,解决掉第一个异步带来的问题了。(相同的核心对数据进行读写,由于异步,很可能会导致第二次读取的还是旧值,所以首先读「store buffer」。

面试官还有其他?

候选者:那当然啊,那「异步化」会导致相同核心读写共享变量有问题,那当然也会导致「不同」核心读写共享变量有问题啊

候选者:CPU1修改了A值,已把修改后值写到「store buffer」并通知CPU2对该值进行invalid(无效)操作,而CPU2可能还没收到invalid(无效)通知,就去做了其他的操作,导致CPU2读到的还是旧值。

候选者:即便CPU2收到了invalid(无效)通知,但CPU1的值还没写到主存,那CPU2再次向主存读取的时候,还是旧值…

候选者:变量之间很多时候是具有「相关性」(a=1;b=0;b=a),这对于CPU又是无感知的…

候选者:总体而言,由于CPU对「缓存一致性协议」进行的异步优化「store buffer」「invalid queue」,很可能导致后面的指令很可能查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作「CPU乱序执行」

候选者:为了解决乱序问题(也可以理解为可见性问题,修改完没有及时同步到其他的CPU),又引出了「内存屏障」的概念。

img

面试官:嗯…

候选者:「内存屏障」其实就是为了解决「异步优化」导致「CPU乱序执行」/「缓存不及时可见」的问题,那怎么解决的呢?嗯,就是把「异步优化」给”禁用“掉(:

候选者:内存屏障可以分为三种类型:写屏障,读屏障以及全能屏障(包含了读写屏障),屏障可以简单理解为:在操作数据的时候,往数据插入一条”特殊的指令”。只要遇到这条指令,那前面的操作都得「完成」。

候选者:那写屏障就可以这样理解:CPU当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。

候选者:通过这种方式就可以让CPU修改的数据可以马上暴露给其他CPU,达到「写操作」可见性的效果。

候选者:那读屏障也是类似的:CPU当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都处理掉

候选者:通过这种方式就可以确保当前CPU的缓存状态是准确的,达到「读操作」一定是读取最新的效果。

img

候选者:由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作。Java封装了一套规范,这套规范就是「Java内存模型」

候选者:再详细地说,「Java内存模型」希望 屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。

img

面试官那要不简单聊聊Java内存模型的规范和内容吧?

候选者:不了,怕一聊就是一个下午,下次吧?

本文总结

  • 并发问题产生的三大根源是「可见性」「有序性」「原子性」

  • 可见性:CPU架构下存在高速缓存,每个核心下的L1/L2高速缓存不共享(不可见)

  • 有序性:主要有三方面可能导致打破

    • 编译器优化导致重排序(编译器可以在不改变单线程程序语义的情况下,可以对代码语句顺序进行调整重新排序)
    • 指令集并行重排序(CPU原生就有可能将指令进行重排)
    • 内存系统重排序(CPU架构下很可能有store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)
  • 原子性:Java的一条语句往往需要多条 CPU 指令完成(i++),由于操作系统的线程切换很可能导致 i++ 操作未完成,其他线程“中途”操作了共享变量 i ,导致最终结果并非我们所期待的。

  • 在CPU层级下,为了解决「缓存一致性」问题,有相关的“锁”来保证,比如“总线锁”和“缓存锁”。

    • 总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。
    • 缓存锁是锁缓存行(cache line),其中比较出名的是MESI协议,对缓存行标记状态,通过“同步通知”的方式,来实现(缓存行)数据的可见性和有序性
    • 但“同步通知”会影响性能,所以会有内存缓冲区(store buffer/invalid queue)来实现「异步」进而提高CPU的工作效率
    • 引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,平日大多数情况下是可以享受「异步」带来的好处的,但少数情况下,需要强「可见性」和「有序性」,只能”禁用”缓存的优化。
    • “禁用”缓存优化在CPU层面下有「内存屏障」,读屏障/写屏障/全能屏障,本质上是插入一条”屏障指令”,使得缓冲区(store buffer/[invalid queue](https://www.zhihu.com/search?q=invalid queue&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={“sourceType”%3A"answer"%2C"sourceId"%3A2215772844}))在屏障指令之前的操作均已被处理,进而达到 读写 在CPU层面上是可见和有序的。
  • 不同的CPU实现的架构和优化均不一样,Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果

写在最后

编程严选网(www.javaedge.cn),程序员的终身学习网站已上线!

如果这篇【文章】有帮助到你,希望可以给【JavaGPT】点个赞👍,创作不易,如果有对【后端技术】、【前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【JavaGPT】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💝💝💝!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

即时设计:设计稿与PPT完美结合,让您的创意作品更具影响力

PPT助手 更多内容 在设计领域,将设计稿与PPT结合起来,可以让您的作品更具吸引力和影响力。为了满足这一需求,我们向您推荐一款强大的设计工具,它可以将设计稿导出为PPT文件,支持线上预览和编辑,让您的创意…

ADS仿真 之 容差/良率分析

之所以要进行容差分析, 是因为任何电子元器件均存在一定的误差, 如电感、电容的精度等。 例如一个标称为2.0nH0.1nH的电感,代表的意思产品有99.74%的概率落在2.0nH0.1nH范围内, 即满足6σ ,σ是标准偏差或者说方差&…

OpenHarmony沙箱文件

一.前言 1.前景提要 DevEcoStudio版本:DevEco Studio 3.1 Release SDK版本:3.2.2.5 API版本:9 2.概念 在openharmony文件管理模块中,按文件所有者分类分为应用文件和用户文件和系统文件。 1)沙箱文件。也叫做应…

C++类和动态内存分配

目录 1. C类的基本概念与使用 2. 动态内存分配与指针 3. 类与动态内存分配的结合应用 4. 注意事项与最佳实践 5.一个简单的示例代码 在C编程中,类是一种重要的概念,它允许我们将数据和操作封装在一起,以实现更加模块化和可维护的代码。而…

运用AI翻译漫画(二)

构建代码 构建这个PC桌面应用,我们需要几个步骤: 在得到第一次的显示结果后,经过测试,有很大可能会根据结果再对界面进行调整,实际上也是一个局部的软件工程中的迭代开发。 界面设计 启动Visual Studio 2017, 创建…

数据结构与算法 - 线性表

文章目录 第1关:实现一个顺序存储的线性表第2关:实现一个链接存储的线性表 第1关:实现一个顺序存储的线性表 编程要求 本关任务是实现 step1/Seqlist.cpp 中的SL_InsAt、SL_DelAt和SL_DelValue三个操作函数,以实现线性表中数据的…

[答疑]领域特定语言DSL属于伪创新吗(谷爱凌)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 Zeyu 2024-1-4 9:20 马丁福勒的领域特定语言DSL是否有阅读的价值?属于伪创新吗? UMLChina潘加宇 这个问题就有点伪创新 ,让人误以为DSL是Fowler发…

本地部署Canal笔记-实现MySQL与ElasticSearch7数据同步

背景 本地搭建canal实现mysql数据到es的简单的数据同步,仅供学习参考 建议首先熟悉一下canal同步方式:https://github.com/alibaba/canal/wiki 前提条件 本地搭建MySQL数据库本地搭建ElasticSearch本地搭建canal-server本地搭建canal-adapter 操作步骤…

shp与数据库(插入数据)

前言 正文 geopandas与shp文件创建表和录入数据 解释一下上面的代码 查询cd2表的geometry字段 查看一下表 前一篇博客的冲突 问题的解决 POLYGON与MUTLIPOLYGON的说明 解决问题的代码 修改表的创建 shapefile和sqlalchemy插入数据 数据库查询 最后 前言 前一篇讲解…

与AI合作 -- 写一个modern c++单例工厂

目录 前言 提问 bard给出的答案 AI答案的问题 要求bard改进 人类智能 AI VS 人类 前言 通过本文读者可以学到modern C单例模式工厂模式的混合体,同时也能看到:如今AI发展到了怎样的智能程度?怎样让AI帮助我们快速完成实现头脑中的想法&…

【hcie-cloud】【17】华为云Stack灾备服务介绍【灾备方案概述、备份解决方案介绍】【上】

文章目录 前言灾备方案概述灾备的定义灾备的重要性故障和灾难对业务连续性带来的挑战灾备系统的衡量指标RTO与RPO分析 灾备等级标准数据中心容灾解决方案全景图云灾备服务总结架构华为云Stack灾备服务总览 备份解决方案介绍云备份服务介绍备份服务架构介绍云备份服务组件功能介…

界面原型设计工具有哪些?看看这9个

界面原型设计是现代设计师必备的技能之一。在设计数字产品或应用程序时,界面原型是将概念转化为具体可交互界面的重要步骤。对于新手小白来说,选择一款易于上手且功能强大的界面原型设计工具至关重要。本文将介绍 9 个常用的界面原型设计工具&#xff0c…

计算机体系结构动态调度(计分板及Tomasulo)学习记录

1.动态调度核心思想:允许就绪指令越过前方停顿指令,提前进入运行(乱序执行) 就绪指令指不存在资源冲突、操作数已就绪的指令,例如,计分板算法使用计分板来实现,Tomasulo使用保留站来实现&#…

苹果电脑交互式原型设计软件Axure RP 9 mac特色介绍

Axure RP 9 for Mac是一款交互式原型设计软件,使用axure rp9以最佳的方式展示您的作品,优化现代浏览器并为现代工作流程设计。同时确保您的解决方案正确完整地构建。Axure RP 9 for Mac为您整理笔记,将其分配给UI元素,并合并屏幕注…

swing快速入门(三十九)进度对话框

🎁注释很详细,直接上代码 上一篇 🧧新增内容 🧨1.模拟耗时操作 🧨2.使用计时器更新进度对话框 🎀源码: package swing31_40;import javax.swing.*; import java.awt.event.ActionEvent; import …

【debug】为什么ansible中使用command出错

碎碎念 在使用ansible执行command的时候,遇到执行会出错的command 比如执行source打算读取环境变量的时候 错误提示为: 没有那个文件或目录:source 一开始以为是错误提示有问题,一直在testrc的路径上检查,但是同样一行命令使用…

C++八股学习心得.8

1.const知道吗?解释其作用 const 修饰类的成员变量,表示成员常量,不能被修改。 const修饰函数承诺在本函数内部不会修改类内的数据成员,不会调用其它非 const 成员函数。 如果 const 构成函数重载,const 对象只能调…

聊聊 Java 集合框架中的 ArrayList

其实 Java 集合框架也叫做容器,主要由两大接口派生而来,一个是 collection,主要存放对象的集合。另外一个是Map, 存储着键值对(两个对象)的映射表。 下面就来说说 List接口,List存储的元素是有序、可重复的。其下有三个…

配置git服务器

第一步: jdk环境配置 (1)搜索【高级系统设置】,选择【高级】选项卡,点【环境变量】 (2)在【系统变量】里面,点击【新建】 (3)添加JAVA_HOME环境变量JAVA_HO…

展台搭建的基本要求

一、选址及总平面布置 展厅人流量大,对选址和总平面布置的要求如下:展厅选址应位于市内或郊区交通便利的区域。大型展览馆应有足够的群众活动广场和停车面积 ,并应有室外陈列场地。室外场地要考虑环境的绿化和美化。各功能分区之间联系方便又互不干扰。建…
最新文章