最近学习Java多线程知识,帆哥出去面试的时候问的最多的就是什么volatile、synchronized等问题。而且还是往深的问的。说到volatile解决的主要问题,就是可见性问题了。由于CPU的缓存机制。导致A线程修改对于线程B不是立即可见的。今天我就从操作系统的角度去整理一下原因。
CPU的缓存
现代的CPU都是多核的。每个核心都有缓存。缓存一般分为3级。L1、L2、L3,其中L1和L2是核心独享,L3多核心共享的。
- L1距离CPU核心最近,容量最小。且分为指令缓存和数据缓存
- L2也是核心独享,但是不分种类,容量更大一些
-
Cache Line
是Cpu从内存中读取数据的基本单元。
组成部分:- Tag(标志)
- Data Block(数据块)
内存数据的写入方式
CPU修改数据后,更新缓存和内存中的值。具体写入方式有:
- 数据修改后,同步回缓存
- 缓存块脏:将此缓存块写入内存,再将缓存块值修改成自己的,且标记脏
- 缓存块不脏:直接缓存块改为自己,且标记为脏
缓存一致性问题
L1和L2是独享的,那么怎么保证CPU核1和核2在自己的L1、L2级独享缓存中看到同一个数据是相同的呢?
缓存不一致例子
采用写回的方式同步内存数据
- 线程A修改了i变量并同步到自己的独享缓存中(写回策略还没有同步到内存中)。
-
缓存一致性的条件
写传播:线程在CPU某核中的缓存更新,一定要同步到其他的核心缓存中
- 事务的串行化:多个操作事务,不同CPU核所看到的操作顺序要一致。先事务A->事务B->事务C
写传播的实现
总线嗅探。某个Cpu核心修改完自己的缓存后,通过总线把修改事件传播到其他的核心上。这就实现了写传播。
那么基于总线嗅探的方式,又通过MESI协议,实现了CPU缓存一致性
MESI协议
- Modified:已修改
- Exclusive: 独占
- Shared: 共享
- Invalidated:已失效
这四个状态就是用来标记 CacheLine的状态。
- 【已修改】:是一个脏标记,缓存已经同步但是没有同步到内存
- 【独占】:数据是干净的。且只存储在一个核的缓存中,此时写入无需通知其他CPU核
- 【共享】:由【独占】转换而来,数据是干净,被多个核心缓存
【无效】:某个核心需要修改缓存是,先将其他的核心该数据缓存标记为无效
走一个小例子
CPU A核、变量i
A核读取变量i,此时 【A核的Cache Line】为【独占】
- A和修改变量i,直接修改,无需总线通知
- 其他核读取变量i,此时【A核的Cache Line】状态 【独占】->【共享】,其他核为【共享】
- A和修改了变量i,再写回【A核的CacheLine】前,通过总线通知其他核,标记【其他核的CacheLine】为【共享】->【已失效】。最后A核写回,【A核的CacheLine】状态变为【已修改】
- A再次修改变量i,无需改状态直接修改
- 其他核读取变量i,通知A核写回数据到内存,并把【A核Cpu Line】状态变为【共享】,其他核心从内存中读取后,它们的CPU Line为【共享】