CAS比较并交换(Compare and Swap)
1 基本原理
有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS是一种典型的乐观锁, 假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
2 实现原子操作
CAS操作分为获取当前值、比较和设置至少两部分,不是原子操作如何保证原子性呢。
2.1 JNI
compareAndSwapInt是借助C来调用CPU底层指令实现的,程序会根据当前处理器的类型来决定是否为cmpxchg指令加上lock前缀,也就是在多处理器上运行是提供内存屏障效果
2.1.1 原子执行
确保对内存的读-改-写操作原子执行
锁住总线
在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存
优化
如果要访问的内存区域已经在处理器内部的缓存中被锁定(E或M),并且该内存区域被完全包含在单个缓存行中,将直接执行该指令
锁定CacheLine
由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性
2.1.2 禁止重排序
禁止该指令与之前和之后的读和写指令重排序
2.1.3 刷新内存
把写缓冲区中的所有数据刷新到内存中
3 缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题
ABA问题
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
解决:使用版本号( AtomicStampedReference)
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
解决:JVM能支持处理器提供的pause指令那么效率会有一定的提升,也可以破坏循环或者将粒度变小,将一个变量拆分为多个变量
一个共享变量
只能保证一个共享变量的原子操作
解决:这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作( AtomicReference)
MESI缓存一致性协议
多核CPU的情况下有多个一级缓存,如何保证内部数据的一致,就引出了缓存一致协议
1 CPU为何要有高速缓存
摩尔定律导致内存和硬盘的发展速度远远不及CPU
2 局部性原理
在CPU访问存储设备时,无论是存取数据或指令,都趋向于聚集在一片连续的区域中
2.1 时间局部性
如果一个信息项正在被访问,那么在近期它很可能还会被再次访问,比如循环、递归、方法的反复调用等
2.2 空间局部性
如果一个存储器的位置被引用,那么将来它附近的位置也很可能被引用,比如顺序执行的代码、连续创建的两个对象、数组等
3 CPU计算流程
带有高速缓存的CPU执行计算的流程
加载到主内存:程序以及数据被加载到主内存
加载到高速缓存:指令和数据被加载到CPU的高速缓存
写到高速缓存:CPU执行指令,把结果写到高速缓存
写回主内存:高速缓存中的数据写回到主内存
4 多级缓存
由于CPU的运算速度超越了一级缓存的数据I/O能力,CPU厂商又引入了多级的缓存结构
一级缓存:也叫内部缓存,封闭在CPU芯片内部,用于暂时存放CPU运算时的部分指令和数据,存取速度与CPU主频一致。
二级缓存(多级类似):也叫外部缓存,位于CPU外部,是一级缓存的缓冲器,存储那些CPU处理时需要用到、一级缓存又无法存储的数据。不能存储原始指令
5 缓存状态
M 修改
该缓存行只被缓存在该CPU缓存中,并且和主存数据不一致,需要在未来的某个时间点(允许其他CPU读取主存相应内容之前)写回主存,变成E
监听主存读取:必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须将该缓存写回主存
E 独享
该缓存行只被缓存在该CPU的缓存中,是未被修改过的,与主存的数据一致,当有其他CPU读取该内存时,变成S,当该CPU修改该缓存行时变成M
监听主存读取:监听内容同M,监听到后需要把缓存行设置为S
优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成I状态,而修改E状态的缓存不需要使用总线事务
S 共享
该缓存行可能(作废)被多个CPU进行缓存,并且与主存一致,当有一个CPU修改该缓存行时,其他变为I
监听无效和独享:必须时刻监听使该缓存行无效或者独享该缓存行的请求,设置为I
不精确:缓存作废时不会广播,同时缓存不会保存该缓存的copy数量,因此无法确定是否已独享
I 作废
缓存作废时不会广播,同时缓存不会保存该缓存的copy数量,因此无法确定是否已独享
6 四个操作
local read读本地缓存
本地失效解除:一个无效的缓存必须从主存中读取(变为E或S),其他的本身即满足
local write写本地缓存
本地M化:本地缓存变M,S时须将其他缓存中该缓存行置为I,该操作经常用广播的方式来完成
其它无效:可以随时将一个非M状态的缓存行作废,或者变成I状态,M状态的必须先被写回主存
remote read读其它缓存
本地M/E共享:M或E会变成S,其它不变,注意,M会先同步到主存
remote write写其它缓存
本地无效:其它的写会导致所有状态的本地无效
7 MESI状态迁移表
AMD的Opteron处理器使用从MESI中演化出的MOESI协议,O(Owned)是MESI中S和M的一个合体,表示本Cache line被修改,和内存中的数据不一致,不过其它的核可以有这份数据的拷贝,状态为S。
Intel的core i7处理器使用从MESI中演化出的MESIF协议,F(Forward)从Share中演化而来,一个Cache line如果是Forward状态,它可以把数据直接传给其它内核的Cache,而Share则不能。
7 存在问题
阻塞问题
缓存切换状态时CPU会等待所有缓存响应完成,可能出现各种各样的性能问题和稳定性问题,比如你需要修改本地缓存中的一条信息,那么你必须将I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。因为这个等待远远比一个指令的执行时间长的多。
存储缓存(store bufferes)
处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
这么做有两个风险
第一、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。这会导致“伪重排序”的问题
第二、保存什么时候会完成,这个并没有任何保证。
失效队列
处理失效的缓存也不是简单的操作,它需要处理器去处理。并且存储缓存也不是无限大的,那么当存储缓存满的时候,处理器还是要等待失效响应的,解决这种问题的思路是让invalidate ack能更早得返回。
入列:收到失效消息时,放到失效队列中去。
回复失效响应:为了不让处理器久等失效响应,收到失效消息需要马上回复失效响应。
等待一同处理:为了不频繁阻塞处理器,不会马上读主存以及设置缓存为invalid,合适的时候再一块处理失效队列。
内存屏障
存储缓存和失效队列会导致缓存问题,即可见性和顺序性,处理器并不知道什么时候优化是允许的,而什么时候并不允许。干脆处理器将这个任务丢给了写代码的人。这就是内存屏障
写之前提交存储缓存:写屏障Store Memory Barrier是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
读之前应用失效队列:读屏障Load Memory Barrier是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
伪共享
在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享
举例:在多线程情况下,x,y两个共享变量在同一个缓存行中,核a修改变量x,会导致核b中的y变量同时失效
补齐:我们只需要前后填几个无用的变量补上>=64-N字节, 让不同的Volatile对象处于不同的缓存行, 就可以避免伪共享了
注解:Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类或字段会自动补齐缓存行,此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效
8 MESI缓存一致性协议失效的原因
失效后果
当MESI失效之后,那么系统会自动将启用总线加锁机制,那么执行效率则会大打折扣。
失效情况
1.数据长度:当缓存行存储的数据超过最小存储单元大小时(数据长度存储跨越多个缓存行的情况),就会导致MESI操作缓存行无效,导致MESI缓存一致性协议失效;
2.不支持:系统不支持缓存一致性协议。
CAS和MESI和VOALTILE
1 volatile如何保证可见性
volatile,是怎么可见性的问题(CPU缓存),那么他是怎么解决的?加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,它有三个功能:
重排序
确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面
立即刷新
将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则保证);
无效嗅探(缓存失效)
会写内存(M-E)触发MESI local write导致其它缓存行无效remote write(也是由volatile先行发生原则保证);
2 CAS保证原子性
2.1 lock cmpxchg
在x86架构上,CAS被翻译为”lock cmpxchg…“,当两个core同时执行对同一地址的CAS指令时其实是在试图修改每个core持有的Cache line
2.2 锁定缓存行
由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性
2.3 总线仲裁协议
对于我们的CAS操作来说, 其实锁并没有消失,只是转嫁到了ring bus的总线仲裁协议中,只不过这个锁获取失败后不会等待
竞争invalidate
如果要由 S转为E或者M,两个core都会向ring bus发出 invalidate这个操作,在ringbus上就会仲裁谁能赢得这个invalidate
失败读取新值
胜者完成操作, 失败者需要接受结果, invalidate自己对应的cacheline,再读取胜者修改后的值, 回到起点
3 MESI和volatile
有了MESI协议,为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在才会触发多核缓存一致性协议?
弱一致性
多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,不能保证一个线程修改变量后,其他线程立马可见
存储缓存等导致延迟:也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存
立即刷新:如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存;
MESI需要触发
另一种解释,正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。