美文网首页多线程JVM
[Java多线程编程之四] CPU缓存和内存屏障

[Java多线程编程之四] CPU缓存和内存屏障

作者: 小胡_鸭 | 来源:发表于2019-10-10 19:58 被阅读0次

    一、CPU三级缓存

    1、缓存的作用

      CPU的结构很复杂,简单地说由运算器和寄存器组成。程序运行时,需要CPU去执行运算,运算是由运算器来执行,运算器可以做加减乘除运算以及与或非逻辑运算,运算过程中可能需要临时存放数据到某个地方,寄存器就起到这个作用。



      虽然寄存器可以存储一些运行时数据,但是容量是很小的,程序运行时产生的大部分数据(比如Java对象)是存储在内存中的,并且程序指令也是存储在内存中,所以程序运行时CPU需要频繁操作内存,包括读取和写入,但是CPU的速度太快了,如果直接操作内存,CPU的大部分时间会处于等待内存操作的空转状态,内存完全跟不上节奏,怎么办?



      这时候就需要有缓存的存在了,内存将CPU要读取的数据源源不断地加载到缓存中,CPU读取缓存,缓存的速度比内存快多了,勉强能跟得上CPU大哥的节奏了!

      但是CPU表示缓存你还是太慢了,我带不动,所以产生了一级缓存、二级缓存、三级缓存,一级缓存最快、二级次之、三级最慢;缓存容量则反过来,一级最小,二级大一些,三级最大。
      为什么缓存能加快系统运行?举个例子,现在需要很多水,如果直接打开水龙头,要放很久,如果有水桶已经放满了水,取水是不是会快点?如果需要更多的水,我们弄个水塔,平时储满水,假如水桶的水不够用,则打开水塔,这样就达到快速取水的目的。
      缓存可以看成是一个数据的池子,由于速度越快的缓存单位存储空间的价格也越高,所以要有多级缓存,速度快的存储小,速度慢的存储大,多级缓存结合达到总体上经济又实惠的效果,在三级缓存中,每一级缓存都有80%左右的命中率,如果本级缓存中找不到CPU要的数据,则进入下一级缓存中查找,三级缓存中找不到则进入内存查找,这种可能性只有0.8%,大多数情况下可以保证了CPU快速运行,避免内存延迟。


    CPU读取数据顺序
    • L1 Cahce(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存,一般服务器CPU的L1缓存的容量通常在32-4096KB。
    • L2 是由于L1高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存储器,即二级缓存。
    • L3 缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能;具有较大L3缓存的处理器可以提供更有效的文件系统缓存行为及较短信息和处理器队列长度;现在的计算机都内置了L3,并且多核计算机中多个CPU可以共享一个L3缓存,但是每个CPU都会有它自己的L1、L2。


      CPU缓存设计示意图

      CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,最后是外存储器。

    2、缓存同步协议

      对于多核计算机,多个CPU可能会读取同样的数据进行缓存,在经过不同运算之后,最终写入主内存,那么问题来了,写入的时候谁先谁后,最终写入主内存中的数据以哪个CPU为准?
      为了应对这种高速缓存回写的场景,众多CPU厂商联合制定了缓存一致性协议MESI协议,并分别实现,MESI协议规定每条缓存有个状态位,同时定义了下面四个状态:

    • 修改态(Modified)- 此cache行已被修改过(脏行),内容已不同于主存,为此cache专有;
    • 专有态(Exclusive)- 此cache行内容同于主存,但不出现于其他cache中;
    • 共享态(Shared)- 此cache行内容同于主存,但也出现于其他cache中;
    • 无效态(Invalid)- 此cache行内容无效(空行)。

      当计算机中有多个处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU;这意味着CPU不仅要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致。

    3、高速缓存存在问题

      缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的;在同一时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。

    二、性能优化 - 运行时指令重排

      运行时指令重排是CPU为了避免阻塞等待某些操作需要的资源,先去执行可执行的指令,当阻塞等待的资源获取到时,再去执行对应的指令的操作

    1、代码示例
    指令重排序
      指令重排的场景:当CPU写缓存时发现缓存区块正在被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。
    【注意】对于单线程程序来说,需要遵循as-if-serial语义,即不管指令如何被CPU重排,最终执行的效果都是一致的。

    2、as-if-serial语义

      不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变,编译器、runtime和处理器都必须遵循as-if-serial语义,也就是说,编译器和处理器不会对存在数据依赖关系的操作做重排序。

    3、指令重排存在问题

      但是对多线程程序来说,指令逻辑无法分辨因果关联,因此指令重排可能会出现乱序执行,导致程序运行结果错误,因此在多线程程序中有些时候需要通类似volatile修饰变量之类的方式来避免指令重排。

    三、内存屏障

      为了解决高速缓存导致的缓存内存数据一致性问题,以及指令重排导致的程序乱序出错问题,处理器提供了两个内存屏障指令(Memory Barrier)。

    1、写内存屏障(Store Memory Barrier)

      在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见;当发生这种强制写入主内存的显式调用,CPU就不会处于性能优化考虑进行指令重排。

    2、读内存屏障(Load Memory Barrier)

      在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据,让CPU缓存与主内存保持一致,避免缓存导致的一致性问题。

    相关文章

      网友评论

        本文标题:[Java多线程编程之四] CPU缓存和内存屏障

        本文链接:https://www.haomeiwen.com/subject/btlnpctx.html