当前位置 博文首页 > 立志欲坚不欲锐,成功在久不在速度:并发编程-JMM内存模型

    立志欲坚不欲锐,成功在久不在速度:并发编程-JMM内存模型

    作者:[db:作者] 时间:2021-07-16 13:15

    目录

    什么是JMM内存模型:

    JMM和JVM有什么不同?

    JMM规范规定了什么 ?

    硬件内存模型:

    为什么需要保证缓存一致性:

    如何解决:

    1. 总线锁定:

    2. 缓存一致性协议

    MESI协议:

    缓冲行:

    伪共享:


    什么是JMM内存模型:

    ????????Java内存模型(Java?Memory?Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

    JMM和JVM有什么不同?

    ????????JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

    JMM规范规定了什么 ?

    ????????JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

    硬件内存模型:

    image.png

    CPU:

    ????????现在的计算机通常由两个或者多个CPU,从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

    CPU寄存器:

    ????????每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存

    高速缓存Cache:

    ????????将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

    内存:

    ????????一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。

    运作原理:

    ????????通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

    看似没有什么问题,但是在多线程下就会存在问题:

    缓存不一致性问题:

    ????????在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况

    为什么需要保证缓存一致性:

    ? ? ??我们知道,CPU和物理内存之间的通信速度远慢于CPU的处理速度,所以CPU有自己的内部缓存,根据一些规则将内存中的数据读取到内部缓存中来,以加快频繁读取的速度。我们假设在一台PC上只有一个CPU和一份内部缓存,那么所有进程和线程看到的数都是缓存里的数,不会存在问题;但现在服务器通常是多 CPU,更普遍的是,每块CPU里有多个内核,而每个内核都维护了自己的缓存,那么这时候多线程并发就会存在缓存不一致性,这会导致严重问题。

    举例:

    ? ? ??以 i++为例,i的初始值是0.那么在开始每块缓存都存储了i的值0,当第一块内核做i++的时候,其缓存中的值变成了1,即使马上回写到主内存,那么在回写之后第二块内核缓存中的i值依然是0,其执行i++,回写到内存就会覆盖第一块内核的操作,使得最终的结果是1,而不是预期中的2.

    image.png

    如何解决:

    1. 总线锁定:

    ????????操作系统提供了总线锁定的机制。前端总线(也叫CPU总线)是所有CPU与芯片组连接的主干道,负责CPU与外界所有部件的通信,包括高速缓存、内存、北桥,其控制总线向各个部件发送控制信号、通过地址总线发送地址信号指定其要访问的部件、通过数据总线双向传输。在CPU1要做 i++操作的时候,其在总线上发出一个LOCK#信号,其他处理器就不能操作缓存了该共享变量内存地址的缓存,也就是阻塞了其他CPU,使该处理器可以独享此共享内存。

    ? ? ? ?但我们只需要对此共享变量的操作是原子就可以了,而总线锁定把CPU和内存的通信给锁住了,使得在锁定期间,其他处理器不能操作其他内存地址的数据,从而开销较大,所以后来的CPU都提供了缓存一致性机制,Intel的奔腾486之后就提供了这种优化。

    2. 缓存一致性协议

    ????????缓存一致性机制就整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取,用MESI阐述原理如下:

    MESI协议:

    是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:

    ? ? ??M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

    ? ? ? ?E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。

    ? ? ? ?S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

    ? ? ? ?I:无效的。本CPU中的这份缓存已经无效。

    总结:当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

    image.png

    执行过程:

    1. 现在有两个线程在运行,内存中有一个变量为a=1
    2. 线程1先从内存中获取到 a,并且将a同步到自己的缓存中,以缓存行为单位,这个时候缓存行的状态为 E (独占当前变量)
    3. 这个时候线程2 也从内存 中同步了变量a ,所以 线程1和线程2中的状态都为 S 共享
    4. 这个时候 线程1对变量a做修改,他 先将 状态 改为 M,此时会缓存锁定? ? ? ? ? ? ? ? ? ? ? ? ? ? ??( 缓存锁定是某个CPU对缓存数据进行更改时,会通知缓存了该数据的该数据的CPU抛弃缓存的数据或者从内存重新读取。)
    5. ),通过嗅探机制使得拥有变量a的线程b 的 状态 变为 I 失效
    6. 当线程1修改完成之后,他的状态会变为 独占状态 E

    缓冲行:

    ????????Cache中的数据是按块读取的,当cpu访问某一个数据的时候,会假设该数据附件的数据以后会被访问到,会将数据存储到缓存中,那么这一块数据称为一个 缓存行目前主流的CPU Cache的Cache Line大小都是64字节。

    cpu读取数据:

    image.png

    CPU到内存之间有很多层的内存,如图所示,CPU需要经过L1,L2,L3及主内存才能读到数据。从主内存读取数据时的过程如下

    • 读取数据
    • 当我左侧的CPU读取x的值的时候,按照下面L1——>L2——>L3,只有下一级没有才会去上一级查找。最后从主内存读入的时候,首先将内存数据读入L3,然后L2最后L1,然后再进行运算。
    • 但是读取的时候,并不是只读一个X的值,而是按块去读取(跟电脑的总线宽度有关,一次读取一块的数据,效率更高)。CPU读取X后,很可能会用到相邻的数据,所以在读X的时候,会把同一块中的Y数据也读进来。这样在用Y的时候,直接从L1中取数据就可以了。
    • 读取的块就叫做缓存行,cache line 。
    • 缓存行越大,局部性空间效率越高,但读取时间慢。
    • 缓存行越小,局部性空间效率越低,但读取时间快。
    • 目前多取一个平衡的值,64字节。
    • 如果你的X和y在同一块缓存行中,且两个字段都用volatile修饰了,
    • 那么将来两个线程再修改的时候,就需要将x和y发生修改的消息告诉另外一个线程,让它重新加载对应缓存,然而另外一个线程并没有使用该缓存行中对应的内容,只是因为缓存行读取的时候跟变量相邻,这就会产生效率问题。
    • 解决起来也简单,我们将数据中的两个volatile之间插入一些无用的内存,将第二个值挤出当前缓存行,那么执行的时候,就不会出现相应问题了。提高代码效率。

    伪共享:

    伪共享的非标准定义为:

    缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

    伪共享是产生过程:

    • CPU1想要修改X,CPU2想要修改Y,这两个频繁改动的变量是同一个缓存行,两个争夺缓存行的拥有权。
    • CPU1抢到后,更新X,那么CPU2上的缓存行的状态就会变成I状态(无效)——状态含义,请看08、MESI协议
    • 当CPU2抢到,更新Y,CPU1上缓存行就会变成I状态(无效)
    • 轮番抢夺,不仅会带来大量的RFO消息,而且某个线程读取此行数据时,L1和L2缓存上都是失效数据,只有L3是同步好的。
    • 表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

    总之伪共享的意思就是: 仅仅只是修改了了变量x,却导致同一个缓存行中的所有变量都无效,需要重新刷缓存

    cs