三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile。单拿一个出来,想必你们对这些概念应该有必定了解。可是这些东西有什么必然的联系,或者他们之间究竟有什么前世此生想必是困扰你们的一个问题。
为何有了MESI协议,咱们还须要volatile?
内存屏障的由来?
指令重排带来的问题?
下面咱们经过分析每个技术的由来,以及带来的负面影响,跟你们探讨一下这些技术之间的联系。具体每一个关键词相关文章也不少再也不赘述,只谈我的理解。html
CPU的发展很快,几乎18-24个月,CPU的计算能力就能翻一番,然而内存的发展却相对缓慢,以致于内存的读写速度远远跟不上CPU的计算能力,也就是说,在CPU和内存的资源交换中,CPU经常须要等待内存,而浪费了大量的计算能力。三级缓存的出现正是为了弥补内存的慢和CPU的快而诞生的产物。
CPU将再也不直接与内存进行数据的交换而是在两者之间加入一个缓存以解决这种不协调而致使的资源浪费状况。固然了缓存必定是比内存的读写速度更快的,否则也没有这个必要了。那是否是咱们直接用缓存就不用内存了,呵呵,钱钱钱。
所谓的三级缓存正是诞生在这样的一个背景下。
三级缓存分为三种:L1 Cache,L2 Cache,L3 Cachejava
内存以及三级缓存的响应时间如上所示,由此可知内存和缓存的差距仍是很大的,当时硬件价格和上图响应时间是成正比的。响应时间越快,价格越贵。程序员
不一样的CPU厂商的架构也有些不一样,在这里只介绍流行的缓存架构缓存
缓存的意义架构
1 时间局限性:若是某个数据被访问,那么它在不久的未来有可能被在次访问 2 空间局限性:若是某个缓存行的数据被访问,那么与之相邻的缓存行很快可能被访问到
缓存的加入是为了解决CPU运算能力和内存读写能力的不匹配问题,简单来讲就是为了提高资源利用率。那么在多CPU(一个CPU对应一个或者多个核心)或者多核心下,每一个核心都会有一个一级缓存或者二级缓存,也就是说一二级缓存是核心独占的(相似JMM模型,线程的工做内存是线程独占的,主内存是共享的)而三级缓存和主内存是共享的,这样就将致使CPU缓存一致性问题。为了解决这种不一致,大名鼎鼎的MESI协议随之而来。并发
MESI协议的由来上个章节已经介绍,在此不作过多介绍app
MESI:Modified(修改),Exclusive(独占),Shared(共享),Invalid(无效)由以上数据的四种状态的首字母而来。
在缓存中数据的存储单元是缓存行(Cache line),主流的CPU缓存行都是64个字节。缓存行的四种状态由两个字节标识。异步
M(Modified)分布式 |
这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。高并发 |
E(Exclusive) |
这行数据有效,数据和内存中的数据一致,数据只存在于本 Cache 中。 |
S(Shared) |
这行数据有效,数据和内存中的数据一致,数据存在于不少 Cache 中。 |
I(Invalid) |
这行数据无效。 |
当一个缓存行的状态调整,另一个缓存行须要调整的状态以下:
M |
E |
S |
I |
|
M |
× |
× |
× |
√ |
E |
× |
× |
× |
√ |
S |
× |
× |
√ |
√ |
I |
√ |
√ |
√ |
√ |
模拟缓存行独占(E)过程:
核心core1发出加载数据指令,经过bus从内存中加载了一个数据A进入到该核心的缓存行,而且发现该数据是没有被别的核心加载的,此时该核心将会将该数据A状态置为独占即E状态。
模拟缓存行共享(S)过程:
上面core1已经独占了数据A,此时core2也发指令要读取数据A,此时发现该数据已经被core1占有,而后通知core1“我也要使用该数据”,因此core1将数据A状态改成S即 E >>> S。
模拟缓存行修改(M)过程:
假若有三个核心:core 1,core 2分别缓存了数据A。由于多个核心都缓存了该数据,即在各个核心中该数据的状态都是Shared。
此时core 1须要修改数据A。
core 1:内核计算完成,经过指令写入缓存行,数据A的状态将从S >>> M
core 2:core1将通知缓存了该数据的核心“我修改了该数据”,因此core2会将该缓存置为无效。由于数据已经发生了变化,core2缓存的数据A将再也不有任何意义。
埋个伏笔,以上过程提到的“通知”的过程多是很耗时的,在此期间core1将会处于等待回应。浪费了core1的计算能力。
多个core的缓存状态置换是须要消耗时间的,致使内核在此期间将无事可作。甚至一旦某一个内核发生阻塞,将会致使其余内核也处于阻塞,从而带来性能和稳定性的极大消耗。
因此这时指令重排开始发挥它的价值。想一想这种等待有时是没有必要的,由于在这个等待时间内内核彻底能够去干一些其余事情。即当内核处于等待状态时,不等待当前指令结束接着去处理下一个指令。
前面介绍了指令重排将会减小处理器的等待时间进而去处理其余的指令。这种一个指令还未结束便去执行其它指令的行为称之为,指令重排。大白话就是指令未按需执行。
Store Buffere---存储缓存
store buffer即存储缓存。位于内核和缓存之间。当处理器须要处理将计算结果写入在缓存中处于shared状态的数据时,须要通知其余内核将该缓存置为 Invalid(无效),引入store buffer后将再也不须要处理器去等待其余内核的响应结果,只须要把修改的数据写到store buffer,通知其余内核,而后当前内核便可去执行其它指令。当收到其余内核的响应结果后,再把store buffer中的数据写回缓存,并修改状态为M。(很相似分布式中,数据一致性保障的异步确认)
Invalidate Queue---失效队列
简单说处理器修改数据时,须要通知其它内核将该缓存中的数据置为Invalid(失效),咱们将该数据放到了Store Buffere处理。那收到失效指令的这些内核会当即处理这种失效消息吗?答案是不会的,由于就算是一个内核缓存了该数据并不意味着立刻要用,这些内核会将失效通知放到Invalidate Queue,而后快速返回Invalidate Acknowledge消息(意思就是尽可能不耽误正在用这个数据的内核正常工做)。后续收到失效通知的内核将会从该queue中逐个处理该命令。(意思就是我也不着急用,因此我也不着急处理)。
指令重排或者说store buffer或者invalidte queue带来的问题就是可见性问题,经过以上分析,咱们不难发现这实际上是保障了数据的最终一致性。由于在处理器对数据的修改不是当即对其余内核可见的,由于修改了的数据被放在了store buffer中,通知其余内核的数据修改也不是达到其余内核并被当即处理的。其实有点异步处理的意思。
数据不一致性问题
假设core1对数据A的修改通知没有被core2当即处理(由于在invalidte queue中),core2紧接着又修改了数据A,是否是就形成了数据的不一致。其它内核对数据的修改对本内核是不可见的。
为了提高性能进入了指令重排,而然指令重排可能会致使数据的不一致,这可如何是好?哈哈哈,固然是有解决方案的,答案就是内存屏障。更多精彩内容且听下回分解。
上一章节中说到指令重排致使的可见性问题可能会致使数据的不一致。CPU就给咱们提供了一直经过软件告知CPU什么指令不能重排,什么指令能重排的机制就是内存屏障。
两个指令:
load:将内存中的数据拷贝到内核的缓存中。
store:将内核缓存的数据刷新到内存中。
内存屏障:Memory Barrier。
内存屏障又分为四种:
LoadLoad Barriers(读屏障),StoreStore Barriers(写屏障),LoadStore Barriers,StoreLoad Barriers
不一样的CPU架构对内存屏障的实现是不尽相同的,咱们这里讨论流行的X86架构。
X86中有三种内存屏障:
Store Memory Barrier:写屏障,等同于前文的StoreStore Barriers
告诉处理器在执行这以后的指令以前,执行全部已经在存储缓存(store buffer)中的修改(M)指令。即:全部store barrier以前的修改(M)指令都是对以后的指令可见。
Load Memory Barrier:读屏障,等同于前文的LoadLoad Barriers
告诉处理器在执行任何的加载前,执行全部已经在失效队列(Invalidte Queues)中的失效(I)指令。即:全部load barrier以前的store指令对以后(本核心和其余核心)的指令都是可见的。
Full Barrier:万能屏障,即Full barrier做用等同于以上两者之和。
即全部store barrier以前的store指令对以后的指令都是可见的,以后(本核心和其余核心)的指令也都是可见的,彻底保证了数据的强一致性。
CPU知道何时须要加入内存屏障,何时不须要吗?CPU将这个加入内存屏障的时机交给了程序员。在java中这个加入内存屏障的命令就是volatile关键字。
澄清一点,volatile并非仅仅加入内存屏障这么简单,加入内存屏障只是volatile内核指令级别的内存语义。
除此以外:volatile还能够禁止编译器的指令重排,由于JVM为了优化性能而且不违反happens-before原则的前提下也会进行指令重排。
并发下的三个概念:
原子性(Atomicity):一个操做是不可中断的,要么所有执行成功要么所有执行失败。
可见性(Visibility):全部线程都能看到共享内存的最新状态。
有序性(Ordering):即程序执行的顺序按照代码的前后顺序执行。
Volatile是JMM(Java Memory Model:java内存模型)中对可见性和有序性的保障。
Volatile内存语义:
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。
有序性:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。其实就是禁止指令重排。
说了这么多终于说到了Volatile的有序性,Volatile正是经过对处理器加入内存屏障来禁止指令重排历来保证有序性的。大白话来讲Volatile就是程序员决定加入内存屏障的指令。
固然在java语言中,指令重排并非只发生在处理器层面。在java编译器中JVM也会存在指令重排优化,以提升程序的性能。Volatile一样能够禁止编译器层面的指令重排。
下面这段话摘自《深刻理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上至关于前文介绍的插入内存屏障指令。(这个lock指令是JMM中的,再往底层就是加入内存屏障指令)
关于JMM,happens-before原则及volatile的其它内容后面打算再写一篇介绍。
最后想说一句,想完全理解Volatile不了解以上那些知识是不够的。
感谢屏幕前的你能看到最后。
右下角推荐,谢谢。
若有错误的地方还请留言指正。
原创不易,转载请注明原文地址:http://www.javashuo.com/article/p-nqjwgkuj-nu.html
参考文献:
https://blog.51cto.com/14220760/2370118?source=dra
https://www.zhihu.com/question/296949412