目录
前言
- 参考/导流:
- 作为Java并发-volatile的前置知识。
- 一定程度上以面试为指导,总结最重要的东西,这里不是完整的总结知识,具体使用中遇到问题请查阅参考博客。
- 一致性问题面试的时候,问的是很多的。
- CPU一致性。
- 分布式一致性。
- 阅读建议:
- 如果基础较弱,建议结合参考博客阅读,这里主要是争对个人情况做的一些笔记,总结重要内容。
遇到的问题——提出问题,方便今后学习的时候提高注意力
- 内存管理:
- 数据写入时,如何定位数据所对应的Cache Block?
- 数据更新时会不会写到L2 Cache,何时写到L2 Cache?
CPU Cache的数据写入
前言
- CPU Cache的结构:
- Cache高速缓存内嵌在CPU中。
- 由多个Cache Line组成的。
- Cache Line是CPU从内存读取数据的基本单位。
- Cache Line由 各种标志(Tag)+数据块(DataBlock)组成。
- 在多核心的处理器里,每个核心都有各自的L1/L2 Cache,而L3 Cache是所有核心共享使用的,与内存直接联系。
- 如何写出让CPU跑得更快的代码?
- 简言之:Cache命中率高的代码。
- 现代,CPU在读写数据的时候,都是在CPU Cache读写数据的。
- 不要考虑下面的写直达的直接写回内存,只是一个概念,现在已经启用了。
- 为保证Cache和内存数据一致,需要将Cache中的数据同步到内存中。
- 写直达、写回
写直达
- 把数据同时写入内存和Cache中。
- 这是保持一致性最简单的方式。
- 步骤:
- 如果数据在CPU Cache中,先写入Cache Block再写入内存;否则直接写入内存。
- 问题:每次写操作都要写回到内存,耗时巨大。
写回
- 发生写操作时,新的数据仅仅被写入Cache Block里,只有当修改过的Cache Block被替换时才需要写到内存中。
- 优化:
- 减少了数据写回内存的频率。
- 流程:
- 检查数据是否在Cache中。在的话直接将数据写入Cache Block并将该块标记为脏。
- 否则定位数据所对应的Cache Block,并查看Cache Block里面的数据是否是脏的。如果是脏的就先将Cache Block中的数据写回到内存再从内存中读取需要的数据到Cache Block。
- 否则直接从内存读取当前要写入的数据到Cache Block。
- 最后将当前数据写入到Cache Block。
- 补充:如有不解,见图 小林coding-2.4 CPU 缓存一致性
- 总结:
- 只有在缓存不命中,且数据对应的Cache Block被标记为脏的情况下才会将数据写回到内存中。
- 好处:
- 如果缓存命中率高,可以极大幅度提升读写性能,因为不用每次都写回到内存。
缓存一致性问题与解决
- 多核心的CPU会带来缓存一致性的问题,因为L1/L2 Cache是多核心各自独有的。
- 缓存一致性问题:
- 假设A,B号核心同时运行两个线程,都操作共同变量i(初始值为0)。
- 为了性能考虑,执行的是 写回策略。A号核心先执行i++,把值为1的结果写入到L1/L2 Cache并标记为脏,但是没有写回内存。
- 然后此时B核心尝试从内存读取i变量的值,则会读到i=0。
- A号核心和B号核心在这个过程中出现了缓存不一致的问题,会导致直接结果的错误——这就是缓存一致性问题。
- 解决缓存一致性必须要保证两点:
- 写传播:某个核心的Cache数据更新时,必须要传播到其他核心的Cache。
- 事务的串行化:某个核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的。
- 比如,A核心的操作a,B核心的操作b,在C和D核心中的顺序不能一个是a->b,一个是b->a。
- 实现事务串行化需要做到2点:
- CPU核心对应Cache中数据的操作,需要同步给其他CPU核心。
- 这难道不就是写传播?但是这里相对于写传播有同步的概念?
- 要引入 锁 的概念,如果两个CPU核心里有相同数据的Cache,那么对于这个Cache数据的更新,只有拿到了锁,才能进行对于的数据更新。
- CPU核心对应Cache中数据的操作,需要同步给其他CPU核心。
总线嗅探——写传播的实现方式
- 介绍:
- 当一个CPU核心修改了L1 Cache中的数据,会通过总线把这个事件广播通知给其他所有的核心。
- 然后每个CPU核心都会监听总线上的广播事件。
- 并检查是否有相同的数据在自己的L1 Cache里面,如果有则更新。
- 总结:
- 广播+监听。
- 问题:
- 总线嗅探只能保证写传播,不能保证事务的串行化。
- CPU需要每时每刻监听总线上的一切活动,且不管别的核心是否缓存相同的数据都会发出一个广播事件,这无疑会加重总线的负载。
- 解决以上问题:
- MESI协议——基于总线嗅探实现了事务串行化,同时用状态机机制降低了总线负载。这个协议做到了CPU缓存一致性。
MESI——基于总线嗅探,真正实现了缓存一致性
- 介绍:
- Modified,已修改。
- Exclusive,独占。
- Shared,共享。
- Invalidated,已失效。
- 4个状态用于标记Cache Line。
- 已修改即脏标记,已经修改但是没有写到内存里。
- 已失效,表示Cache Block的数据已经失效。
- 独占和共享都代表Cache Block里的数据是干净的,也就是说数据和内存中是一致的。
- 区别在于,独占表示数据只存储在一个CPU核心的Cache里,写数据时不需要通知其他CPU核心;
- 共享表示相同的数据在多个CPU核心的Cache里都有,所以在修改前先要想其他CPU核心广播一个请求,把他们的Cache Line标记为无效状态,然后再更新当前Cache里的数据。
- 状态流转表格:详见小林coding-2.4 CPU 缓存一致性
- 这可不是一个很简单的东西,建议记住一些易错的东西(以下总结),然后原则上还是按它们的定义来推论。
- 表格的几点总结(不成系统):
- 一个数据再多核心中一定只有一个已修改M或者独占E。
- 已修改M和共享E区别:已修改M的Cache Line中的数据和内存中是不一样的。
- 只有遇到已修改M,才可能会将Cache Line中的数据写回内存。
- E,S,I都不会,具体自己体会以下秒懂。
- 只有遇到已失效I,才可能会从内存中读取数据到Cache Line。
- E,S,M都不会。
- 遇到已失效I:(容易搞错)
- 在本地核心 读 的时候,会先判断其他核心中是否有这份数据,同时判断其Cache Line状态,如果是已修改M则需要将其写入内存,本地核心再从内存中去,然后两个核心的Cache Line都变成S。
- 如果有,且Cache Line状态为S或者E,本地核心的Cache从内存中取数据,取完之后都标记为S。
- 注意不是从其他核心取,其他核心的数据只有在修改的时候会通过总线嗅探进行写传播。
- 其他核心没有这份数据自然从内存中读取并标记为E。
- 在本地核心 写 的时候,主要流程是从内存中取数据,缓存到Cache中,将其标记为M。
- 另种特殊情况:
- 如果其他核心的Cache有这份数据,且状态为M,则在从内存中取数据之前需要将其他核心里面的数据写入内存。
- 如果其他核心的Cache中有这份数据,则在本地Cache中更新数据前需要标记为I。
- 遇到已修改M:
- 写完之后会变成E或者S。
- 一个数据再多核心中一定只有一个已修改M或者独占E。
- 总结:
- E,S都表示有效且一致;
- M表示有效且不一致;
- I表示无效。
- 只要现在理解了,后面理解起来是非常简单的。。。
拓展1:伪共享问题相关
前言
伪共享的问题
- 介绍:
- 如果两个核心持续交替的修改两个变量A、B,且两个变量归属于同一个Cache Line,那么就会导致CPU Cache失效,即每一次操作都会从内存中读取,这种现象称为伪共享。
- 失效的具体细节:
- 将两个变量同时读入到两个核心的同一个Cache Line中,标记为共享。
- 在修改的时候会不断的将另一个核心中的Cache Line置为已失效状态I,将正在操作的Cache Line置为已修改状态M。
- 所以在修改的时候会不断将另一个Cache Line写入到内存再从内存中读取数据到正在操作的核心中的Cache Line。
- 总结:
- 不同数据处于同一块,修改的时候不断将对方置为I,将自己置为M。导致不断的写回到内存和从内存中读取,CPU Cache失效。
- 补充1:
- 很多机器的L1 Cache Line大小为64字节,即可一次性载入的数据大小为64字节,相当于16个int型数据。
避免伪共享
- 简介:字节填充。
- 一般操作系统会有这样的命令。
- 具体实现:
- 将”热点“数据强制分开,通过填充字节。
- 结果是一个L1 Cache Line保证只有一个数据,这样会浪费一定空间,但是可以有效避免伪共享问题。
总结
- 缓存一致性问题:
- 多核心的Cache中数据和内存中数据不一致,各个核心的Cache中数据也不一致,就会造成缓存不一致,出现错误。
- 解决:
- 写传播和事务的串行化。
- 基于总线嗅探的MESI协议可以完美解决这个问题。
- 但是MESI协议有时候会引发伪共享的问题,我们可以通过填充字节来特殊解决。