一直很想说这个问题,垃圾培训机构已经把韭菜们带偏了,网上千篇一律的说volatile和MESI协议没有关系,那究竟有没有关系呢??坐好认真听!
1、缓存一致性问题
由于存储设备与处理器的运算速度差距很大,计算机系统在内存与处理器之间增加了一层高速缓存,将运算需要的数据复制到缓存中,让运算能快速进行。
有了高速缓存的存在以后,每个CPU的处理过程是, 先将计算需要用到的数据缓存在CPU高速缓存中,在CPU 进行计算时,直接从高速缓存中读取数据并且在计算完成 之后写入到缓存中。在整个运算过程完成后,再把缓存中 的数据同步到主内存。
通过高速缓存很好解决了处理器与内存处理速度的矛盾,但是也带来了缓存一致性问题:不同的线程可能运行在不同的CPU内,同一个数据也会被缓存到多个CPU中,在不同CPU中运行的不同线程看到的同一份内存数据的不同值,这就是缓存一致性问题。
2、缓存锁——缓存一致性协议
为了解决缓存一致性问题,引入了缓存锁,缓存锁的核心机制就是缓存一致性协议。常见的就是MESI协议,MESI表示缓存行的四种状态:
* M(modify)表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致。
* E(exclusive)表示缓存的独占状态,数据只缓存在当前 CPU缓存中,并且没有被修改 。
* S(shared)表示数据可能被多个CPU缓存,并且各个缓 存中的数据和主内存数据一致。
* I(invalid)表示缓存已失效。
在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。各缓存通过状态机制来实现数据的一致性。
MESI协议带来的问题:就是各个CPU缓存行的状态是通过消息传递来进行的。如果 CPU0 要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的CPU。并且要等到他们的确认回执。CPU0 在这段时间内都会处于阻塞状态。
3、可见性的罪魁祸首:store buffers(存储缓存)
为了避免阻塞带来的资源浪费。在cpu中引入了Store Bufferes。CPU在写入共享数据时,直接把数据写入到store bufferes中,同时发送invalidate消息,然后继续去处理其 他指令。当收到其他所有CPU发送了invalidate acknowledge消息 时,再将 store bufferes 中的数据数据存储至 cache line 中。最后再从缓存行同步到主内存。
store buffers带来的问题:引入了storebufferes后,CPU会优先从store buffers中读取数据,这在一些情况下会导致CPU的乱序执行,也可以认为是一种重排序,这种重排序会带来可见性问题。
4、可见性的另外一个帮凶:invalidate queue(无效队列)
在第3步中发送invalidate消息的时候,在某些cpu架构下(x86下没有)是直接发送到一个叫做invalidate queue的组件中的,所以cpu不会等待ack消息返回,而是自己去干其他事情了,所以无效指令还没有立即被执行,所以其他cpu对应的cache里还是脏数据,当cpu再次去读取的时候,此时读取的就不一定是最新数据,所以表现在程序层面就是可见性问题。如下图所示:
5、CPU层面的内存屏障
为了防止store buffers造成的CPU对内存的乱序访问,引入内存屏障来保证数据的可见性。
CPU层面的内存屏障包括读屏障、写屏障、全屏障:
* 写屏障:告诉处理器在写屏障之前的所有已经存储到存储缓存(store buffers)中的数据同步到主内存。
* 读屏障:使高速缓存中的数据失效,强制从主内存中读取数据。
* 全屏障:写屏障+读屏障。
volatile关键字会生成一个Lock汇编指令,这个指令就相当于实现了内存屏障。
5、java内存模型(JMM)
JMM属于语言级别的抽象内存模型,它定义了共享内存中多线程程序读写操作 的行为规范:在虚拟机中把共享变量存储到内存以及从内存中取出共享变量的底层实现细节,通过这些规则来规范对内存的读写操作从而保证指令的正 确性,它解决了CPU多级缓存、处理器优化、指令重排序 导致的内存访问问题,保证了并发场景下的可见性。JMM并没有限制处理器高速缓存和指令重排序,而是在JVM层面通过内存屏障指令和happen-before规则来解决可见性问题,volatile就是其中一种方式。
6、总结
从上面分析可以得出,导致数据可见性问题点的根本原因是:CPU高速缓存以及重排序。
早期mesi协议性能低下,于是引入store buffer和invalidate queue。这就好比应用开发层面引入mq一样,引入之后指令就发生异步了(写指令在storebuffer中,置无效指令在invalidate queue中),所以volatile应运而生,在不需要多线程间通信的情况下,代码安全,效率也很高,此时不需要volatile。但是一旦在多线程间需要通信的时候,此时就不能异步了,必须即时刷新,所以volatile派上用场。
你们TM的说volatile和MESI协议到底有没有关系??!!说没有关系的水货们是因为不知道MESI协议引入storebuffer和invalidate queue之后MESI协议消息不再是强一致性了,此时在需要强一致性的场景下volatile就必然需要了。
7、注意点
volatile只是解决可见性,并没有解决原子性,它的可见性是针对一个原子操作。所以多线程情景下对volatile修饰的变量进行运算是不安全的,这里就不写了。具体知识点可以到这里查看:
https://huatu.98youxi.com/lct/#Raa211ed098d85970d0e67f2434fba345
已录制完成课程目录如下:
专题一、【jdk源码&多线程&高并发】
阶段1、深入多线程 阶段一、深入多线程 - GitMind
阶段2、深入多线程设计模式 阶段二、深入多线程设计模式 - GitMind
阶段3、深入juc源码解析 阶段三、深入juc源码解析 - GitMind
阶段4、深入jdk其余源码解析 未开始
阶段5、深入jvm源码解析 未开始