Java并发编程-CPU缓存知识
基础知识
首先了解程序在系统硬件组织是如何运行,如下方图这是一张Intel系统产品族的模型它和所有其它系统有相似的共同特性。
编译完成的待执行的代码I/O桥会从硬盘读取复制到主存储器(内存)再通过I/O桥复制到CPU,CPU读取复制到的数据会放到寄存器通过ALU(逻辑单元运算器)计算完成返回到寄存器,再由I/O桥把结果返回到主存储器或其它组件。
高速缓存的至关重要
根据机械原理,较大的存储设备要比较小的存储设备运行的慢,而快速设备的造价远高于同类低速设备。比如说,硬盘比主存储器大1000倍,但是对处理器而言,从硬盘上读取一个字的时间开销要比主存储器的读取开销1000万倍。
随着这些年半导体技术进步,处理器与主存的之间的速度差距增大,为了解决系统通信的速度差距所以处理器引入了高速缓存(cache memory),作为暂时的集结区域,存放处理器近期可能会需要的信息。
多级缓存
现在的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层缓存已经满了需要驱逐一个块来替换。
缓存一致性
如果多个CPU同时缓存了一个数据对象并且某个CPU更新了数据,其它CPU是未知的,这会导致数据不一致。当CPU 0、CPU1 、CPU2 同时去执行x这个变量自增一次时:如果CPU0 自增完毕了其它CPU是不知道CPU0运算结果,会导致所有最终结果X=2,这并不是理想的结果。
多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。
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缓存比较友善的代码。