并发编程之JMM内存模型(一)— 底层实现原理

一丶前言

什么是JMM? 

JMM(Java Memory Model)即Java内存模型,Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,如果高速缓存不经过内存的时候,也是不可见的一种表现。在Java程序中,内存本身是比较昂贵的资源,其实不仅仅针对Java应用程序,对操作系统本身而言内存也属于昂贵资源,Java程序在性能开销过程中有几个比较典型的可控制的来源。synchronized和volatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(memory barrier)的指令,来刷新缓存,使缓存无效,刷新硬件的写缓存并且延迟执行的传递过程,无疑该机制会对Java程序的性能产生一定的影响。


JMM的最初目的,就是为了能够支持多线程程序设计的,每个线程可以认为是和其他线程不同的CPU上运行,或者对于多处理器的机器而言,该模型需要实现的就是使得每一个线程就像运行在不同的机器、不同的CPU或者本身就不同的线程上一样,这种情况实际上在项目开发中是常见的。对于CPU本身而言,不能直接访问其他CPU的寄存器,模型必须通过某种定义规则来使得线程和线程在工作内存中进行相互调用而实现CPU本身对其他CPU、或者说线程对其他线程的内存中资源的访问,而表现这种规则的运行环境一般为运行该程序的运行宿主环境(操作系统、服务器、分布式系统等),而程序本身表现就依赖于编写该程序的语言特性,这里也就是说用Java编写的应用程序在内存管理中的实现就是遵循其部分原则,也就是前边提及到的JMM定义了Java语言针对内存的一些的相关规则。然而,虽然设计之初是为了能够更好支持多线程,但是该模型的应用和实现当然不局限于多处理器,而在JVM编译器编译Java编写的程序的时候以及运行期执行该程序的时候,对于单CPU的系统而言,这种规则也是有效的,这就是是上边提到的线程和线程之间的内存策略。JMM本身在描述过程没有提过具体的内存地址以及在实现该策略中的实现方法是由JVM的哪一个环节(编译器、处理器、缓存控制器、其他)提供的机制来实现的,甚至针对一个开发非常熟悉的程序员,也不一定能够了解它内部对于类、对象、方法以及相关内容的一些具体可见的物理结构。相反,JMM定义了一个线程与主存之间的抽象关系,其实从上边的图可以知道,每一个线程可以抽象成为一个工作内存(抽象的高速缓存和寄存器),其中存储了Java的一些值,该模型保证了Java里面的属性、方法、字段存在一定的数学特性,按照该特性,该模型存储了对应的一些内容,并且针对这些内容进行了一定的序列化以及存储排序操作,这样使得Java对象在工作内存里面被JVM顺利调用,(当然这是比较抽象的一种解释)既然如此,大多数JMM的规则在实现的时候,必须使得主存和工作内存之间的通信能够得以保证,而且不能违反内存模型本身的结构,这是语言在设计之处必须考虑到的针对内存的一种设计方法。这里需要知道的一点是,这一切的操作在Java语言里面都是依靠Java语言自身来操作的,因为Java针对开发人员而言,内存的管理在不需要手动操作的情况下本身存在内存的管理策略,这也是Java自己进行内存管理的一种优势。(以上摘自百度百科)

总结:JMM是一个抽象的规范,方便Java多线程对应到计算机底层CPU实现。

二丶CPU对多线程的实现过程

上图是CPU从主内存中加载数据修改完成并回写到主内存中的一个流程图,下面我们讲一下该图中的几个主要部分,然后接着讲具体流程

1.关键词解释

1.1 现代计算机结构模型是冯·诺依曼。

1.2 主内存:RAM 也就是我们所说的内存条。

1.3 CPU:中央处理器,其中包含3块高速缓存区(分别是L1,L2,L3;其中L3的空间大小>L2>L1>寄存器,反之速度排名 寄存器>L1>L2>L3),以及控制单元,运算单元,存储单元,根据不同厂家不同型号,内部的结构也不同。

1.4 I/O总线:总线是作用于传递交互各个组件之间的通信必经之路,比如说CPU要从主内存中获取一块数据,就要通过总线来获取。

1.5 MESI:缓存一致性协议,分为四种状态 E-独占,S-共享,M-修改,I-失效,主要作用是确保多线程同时修改一条数据的时候,数据不会出现覆盖的情况。

1.6 缓存行:CPU缓存区中缓存数据的单位(部分CPU可能不支持缓存行)。

1.7 原子性:不可分割的操作,开始执行就必须全部执行完成。

1.8 可见性:在多线程中,其中一个线程修改了多线程共用的变量,其他线程可以感知到修改。

1.9 有序性:按照代码的顺序从上到下执行指令。

2.运行过程

假设主内存中有数据 x=2,我们要执行x++这句代码的时候,计算机会怎么做呢?

如果是单线程的情况下CPU接到指令后会检查自己的缓存区中是否有x,首先检查寄存器,如果没有再去检查L1,L2,L3,如果缓存区中不存在则通知总线需要去主内存中获取x的值,然后获取到x后,会将x首先放进L3,其次放进L2。然后放进L1,最后放进寄存器中,控制单元将指令发送给运算单元,运算单元完成x++的操作,再丢给存储单元,存储单元写回给L1->L2->L3->总线->主内存,到此完成修改。

那如果是多线程呢?

多线程的情况下,我们不确定多线程是否来源于同一块CPU,我们以最复杂的情况来讲,假设2块CPU,每块CPU持有1个线程,共同修改x,按照正常的逻辑两个线程执行完毕x的值应该是4;

线程1和线程2同时获取到了主内存中的x,在两个线程中x的值都是2,经过上面的运算那两个线程得出的结果都是3,最后写回到主内存中,那结果一定是3,这样显然是不对的,那要怎样解决这个问题呢?

使用volatile关键字!!!

 volatile可以保证有序性和可见性,因为volatile在底层执行时,会允许使用缓存一致性协议来保证操作的可见性,使用了缓存一致性协议之后那么执行过程就变成了这样:

线程1获取到x加进CPU-A缓存行中,此时因为缓存一致性协议,该缓存行状态为E-独占,并开启总线嗅探机制(持续监听总线),此时线程2获取到x加进CPU-B缓存行中,也开启嗅探总线,线程1和线程2都嗅探到对方使用了x,那么两个线程都将状态变成S-共享,然后进行运算,运算完成后优先写回到缓存行的那个线程将缓存行状态改变成M-修改,并通知总线将其他线程的x缓存行变更成I-无效,完成一系列动作之后等待某一时机写回主内存,完成操作。

注意:如果开启缓存一致性协议后,出现同时获取到x,运算完成后同时写回缓存行,这种情况会进行裁决,类似于投票,选择出一个线程让他改变状态。

内容来源:http://www.tengspace.com/index.php/2019/08/03/%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e4%b9%8bjmm%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b%ef%bc%88%e4%b8%80%ef%bc%89-%e5%ba%95%e5%b1%82%e5%ae%9e%e7%8e%b0%e5%8e%9f%e7%90%86/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值