1. 最近学习Java多线程知识,帆哥出去面试的时候问的最多的就是什么volatilesynchronized等问题。而且还是往深的问的。说到volatile解决的主要问题,就是可见性问题了。由于CPU的缓存机制。导致A线程修改对于线程B不是立即可见的。今天我就从操作系统的角度去整理一下原因。

CPU的缓存

现代的CPU都是多核的。每个核心都有缓存。缓存一般分为3级。L1、L2、L3,其中L1和L2是核心独享,L3多核心共享的。
image.png

  • L1距离CPU核心最近,容量最小。且分为指令缓存和数据缓存
  • L2也是核心独享,但是不分种类,容量更大一些
  • L3距离最远,多核心共享,容量在缓存中最大

    Cache Line

    是Cpu从内存中读取数据的基本单元。
    组成部分:

    • Tag(标志)
    • Data Block(数据块)

内存数据的写入方式

CPU修改数据后,更新缓存和内存中的值。具体写入方式有:

  • 写直达
  • 写回

    写直达

    image.png

  • Cache中是否有这个数据,有的话改掉,没有则罢

  • 更新数据到内存中,over

    写回

    写直达呢,每次都需要同步数据到内存中,但是写回则不一样,先修改缓存,必要时同步回内存
  • 数据修改后,同步回缓存
    • 缓存块脏:将此缓存块写入内存,再将缓存块值修改成自己的,且标记脏
    • 缓存块不脏:直接缓存块改为自己,且标记为脏

缓存一致性问题

L1和L2是独享的,那么怎么保证CPU核1和核2在自己的L1、L2级独享缓存中看到同一个数据是相同的呢?

缓存不一致例子

采用写回的方式同步内存数据

  • 线程A修改了i变量并同步到自己的独享缓存中(写回策略还没有同步到内存中)。
  • 此时,线程B读取了内存中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为【共享】