基础知识

首先了解程序系统硬件组织是如何运行,如下方图这是一张Intel系统产品族的模型它和所有其它系统有相似的共同特性。

系统硬件组织

编译完成的待执行的代码I/O桥会从硬盘读取复制到主存储器(内存)再通过I/O桥复制到CPUCPU读取复制到的数据会放到寄存器通过ALU(逻辑单元运算器)计算完成返回到寄存器,再由I/O桥把结果返回到主存储器或其它组件。

高速缓存的至关重要

根据机械原理,较大的存储设备要比较小的存储设备运行的慢,而快速设备的造价远高于同类低速设备。比如说,硬盘比主存储器大1000倍,但是对处理器而言,从硬盘上读取一个字的时间开销要比主存储器的读取开销1000万倍。

随着这些年半导体技术进步,处理器与主存的之间的速度差距增大,为了解决系统通信的速度差距所以处理器引入了高速缓存(cache memory),作为暂时的集结区域,存放处理器近期可能会需要的信息。

系统硬件组织加入缓存

多级缓存

CPU多核缓存

现在的CPU多核技术,都会有多级缓存,老的CPU会有两级内存(L1和L2),新的CPU会有三级内存(L1,L2,L3 )

  • L1缓分成两种,一种是指令缓存,一种是数据缓存。L2缓存和L3缓存不分指令和数据。
  • L1和L2缓存在每一个CPU核中,L3则是所有CPU核心共享的内存。
  • L1、L2、L3的越离CPU近就越小,速度也越快,越离CPU远,速度也越慢。

我们的数据就从内存向上,先到L3,再到L2,再到L1,最后到寄存器进行CPU计算。建立多级缓存就会引起两个问题缓存的命中率缓存更新的一致性

缓存的命中

关于高速缓存映射参考《深入理解计算机系统》

对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的,对于这样的对象组块(block)构成的组,术语叫“Cache Line”,一般来说,一个主流的CPU的Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),64Bytes也就是16个32位的整型,这就是CPU从内存中复制数据上来的最小数据单位。

存储器层次结构中心思想是,位于L级比L+1级的存储设备更小更快。所以通常L0和L1缓存传输通常为1个字大小的块。L1和L2 (L2和L3、L3和L4)的传送通常使用的是几十个字节的快。缓冲命中就是当程序需要访问第L+1层的某个数据对象时,它首先先在当前存储L层的一个块中查找。如果刚好存在那么说明缓冲命中,就直接可以访问数据对象可比从L+1层访问更快。反之就是L层没有缓存数据对象称为缓冲未命中缓冲未命中 CPU就需要等待L层去复制L+1层的数据对象。如果第L层缓存已经满了需要驱逐一个块来替换。

缓存一致性

MESI 互动动画

如果多个CPU同时缓存了一个数据对象并且某个CPU更新了数据,其它CPU是未知的,这会导致数据不一致。当CPU 0、CPU1 、CPU2 同时去执行x这个变量自增一次时:如果CPU0 自增完毕了其它CPU是不知道CPU0运算结果,会导致所有最终结果X=2,这并不是理想的结果。

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI

缓存一致性0

MESI协议其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的),它们存储在 Cache line

简单示例

当前操作 CPU0 CPU1 内存 说明
1) CPU0 read(x) x=1 (E) x=1 只有一个CPU有 x 变量, 所以,状态是 Exclusive
2) CPU1 read(x) x=1 (S) x=1(S) x=1 有两个CPU都读取 x 变量, 所以状态变成 Shared
3) CPU0 write(x,9) x=9 (M) x=1(I) x=1 变量改变,在CPU0中状态 变成 Modified,在CPU1中 状态变成 Invalid
4) 变量 x 写回内存 x=9 (M) X=1(I) x=9 目前的状态不变
5) CPU1 read(x) x=9 (S) x=9(S) x=9 变量同步到所有的Cache中, 状态回到Shared

最后

Java的内存模型和操作系统内存模型有很多共同特性,每条线程都有自己的缓存(工作内存),对于日后处理Java并发安全有一定的好处。

了解CPU缓存能帮助自己写出一些对CPU缓存比较友善的代码。