背景
伪共享问题的表现是:并发的修改在一个缓存行中的多个独立变量,看起来是并发执行的,但实际在CPU处理的时候,是串行执行的,并发的性能大打折扣。
这个涉及到MESI(缓存一致性协议),参考链接:Cache一致性协议之MESI
伪共享的原因就是 CPU 在 Invalid 的时候,是会直接废除一行的!
如果 两个变量 (a,b) 同时在一个 Cache Line 中,处理器A修改了变量a ,那么处理器B中,这个 CacheLine 失效了,这个时候如果处理器B修改了变量b的话,就必须先提交处理器A的缓存,然后处理器B再去主存中读取数据!这样就出现了问题,a和b在两个处理器上被修改,本应该是一个并行的操作,但是由于缓存一致性,却成为了串行!这样会严重的影响并发的性能!
解决方案
Java中提供给了我们两种方案:
填充法 和 Contended 注解
填充法:就是 扩大对象的大小,这样,就可以一个缓冲行中,只存在一个对象!这样,就不会导致结果是串行执行了!
public class DataPadding{
long a1,a2,a3,a4,a5,a6,a7,a8;//防止与前一个对象产生伪共享
int value;
long modifyTime;
long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相关变量伪共享;
boolean flag;
long c1,c2,c3,c4,c5,c6,c7,c8;//
long createTime;
char key;
long d1,d2,d3,d4,d5,d6,d7,d8;//防止与下一个对象产生伪共享
}
上面的代码使用,填充法,对象的属性在内存行中的布局如下
value , modifyTime
flag
createTime key
Contended 注解法:Java1.8 中提供了Contended注解,使用这个注解,VM必须设置 -XX:-RestrictContended。
如果在类型上添加Contended注解,那么这个类的对象的每个属性都会在不同的CacheLine中
如果在属性上设置Contended,那么可以指定哪些 属性 处于一个CacheLine中
@SuppressWarnings("restriction")
public class ContendedGroupData {
@sun.misc.Contended("group1")
int value;
@sun.misc.Contended("group1")
long modifyTime;
@sun.misc.Contended("group2")
boolean flag;
@sun.misc.Contended("group3")
long createTime;
@sun.misc.Contended("group3")
char key;
}
这个伪共享的注解,在 Thread 和 ConcurrentHashMap 就有用上,对于并发的修改一个对象中的多个属性的时候,应该防止伪共享!
原文连接:并发中的伪共享问题
讨论:
缓存一致性协议保证各CPU内的缓存行数据是否有效,若CPU1,CPU2的相同缓存行都缓存了变量X,两个CPU的缓存行状态为S,那当CPU1修改缓存行中的变量X时,CPU1的缓存行状态变为M,CPU2缓存行状态变为I。当CPU2中读取变量X时,由于缓存行状态为I,会重新从内存中读取,而CPU1会提前把数据刷回内存,保证CPU2读取到的是最新的数据,然后两个CPU的缓存行状态再变为S。
提问:既然有了缓存一致性能保证各CPU之间能读取到最新的数据,那为啥还要用volatile呢?
还是和CPU以及缓存行有关。由于CPU需要跟其他CPU的高速缓存控制器通信来协调同一缓存行的状态改变,通信期间CPU是阻塞的,为了提高CPU的利用率,引入了MOB(Memory Ordering Buffers)来缓存同高速缓存交互的指令,MOB位于CPU和L1缓存之间,对其他CPU不可见。MOB由一个64长度的load buffer和36长度的store buffer组成,如下图所示:
CPU读取数据的指令会先放入load buffer,修改数据的指令先放入store buffer,两个buffer都是FIFO队列结构,存入buffer后CPU立即执行其他的指令,由高速缓存控制器逐一执行buffer中指令,执行完成由高速缓存控制器通知CPU。
为了提高invalid消息的应答效率,引入了invalid buffer队列,即当高速缓存控制器接收到其他控制器发送的invalid消息时会立即发送消息答复,并将对应的invalid操作放入invalid buffer中,然后异步执行该buffer中的invalid操作。注意store buffer对CPU是可见的,即CPU可以读取store buffer中未写入到高速缓存中的数据,但是invalid buffer对CPU是不可见的,即除非invalid buffer中的invalid操作对具体的缓存行生效了,CPU会继续读取已经事实上失效的数据。
MOB通过异步执行读写缓存指令提高了CPU利用率,但是引入了两个新的问题,第一,修改数据的指令在buffer中还未执行或者invalid buffer中的invalid操作未执行时,其他CPU同一缓存行的状态还未改成无效I,此时读取的数据就不是最新的;第二,不同高速缓存控制器执行指令的顺序不一定是CPU写入指令的顺序,比如CPU A 写入load A指令,然后CPU B写入load B指令,实际上可能是load B指令先于load A指令执行,这种执行顺序上的变化称之为指令重排序,在并发的情形下可能会导致异常。
JMM规范中制定的指令重排序规则:
为了解决并发环境下指令重排序可能导致的问题和引入MOB导致的数据未及时更新的问题,CPU提供了内存屏障(memory barrier)指令,不同架构的CPU对应的内存屏障指令不同,通常包含以下四种类型的指令:
而volatile就是解决在load buffer和store buffer中的问题:
Java volatile变量在执行写操作之后由JVM插入一个store屏障,在执行读操作之前插入一个load屏障,从而确保程序读取到的volatile变量始终是最新的,对该变量的修改立即对其他线程可见。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。
讨论总结
伪共享涉及缓存一致性导致的多个CPU共同修改同一缓存行变量的问题,而CPU为了提高利用率,引用了Memory Ordering Buffers来缓存与高速缓存交互的指令。MOB位于CPU与L1缓存之间,由store buffer和load buffer组成。CPU还引入了invalid buffer队列以提高invalid消息的答复。store buffer对CPU可见,invalid对CPU不可见。
由于MOB,当修改数据的指令在buffer中还未执行或者invalid buffer中的invalid操作未执行时,其他CPU同一缓存行的状态还未改成无效I,此时读取的数据就不是最新的。此外还有高速缓存控制器执行指令时的指令重排序问题。所以需要内存屏障。
而Java中的volatile就是解决CPU可能读取load buffer中事实上已经失效的数据。Java volatile变量在执行写操作之后由JVM插入一个store屏障,在执行读操作之前插入一个load屏障,从而确保程序读取到的volatile变量始终是最新的,对该变量的修改立即对其他线程可见。即伪共享问题涉及多个CPU读取同一缓存行变量,而volatile是解决CPU可能读取load buffer中事实上已经失效的数据。
参考连接: