在java并发编程中,要保证可见性,需要给变量增加volatile关键字。
普通变量存在可见性的原因是多核CPU架构中各处理器缓存不一致造成的,需要解决缓存不一致问题,才能保证可见性,总线窥探就是实现缓存一致性的方案之一。
缓存不一致
1、cpu1和cpu2都读取了x变量
2、cpu1对x变量进行了+100操作, 操作完成后暂时写到缓存中,此时x应该为200.
3、但cpu2的缓存内x仍然是100,产生了缓存不一致问题
4、最终会导致x值不准确,可能是200,也可能还是100。
总线窥探机制 bus snooping
确保缓存一致性的解决方案有两种
总线窥探机制,受制于带宽的限制,只能用于64个核心以内的情况
基于目录的机制,用于大于64个核心的情况
原理
窥探者监视总线每一个修改操作,如果发现了修改操作,就会检查被修改的值是否也同样存在其他缓存内,如果发现了有副本,则使副本失效,然后将被修改的值尽快写回到主存。 那些失效的副本所在处理器,就不得不再去主存重新加载。
窥探协议之一MESI协议
协议存在4个状态,分别为:
M:修改,监测到修改操作时,会对该缓存标记为E,如果其他处理器有该值副本,会被标记为I;
E:独享,只有一个处理器加载了;
S:共享,有多个处理器加载了同一个值,但还没进行写操作;
I:失效,当有E出现时,其他缓存副本会被当失效处理
伪共享问题
高速缓存中,被分为了缓存行(64byte),一次会加载,会将邻近数据也加载,加速cpu操作(局部性原理)。当同一行中的多个变量,在不同的处理器中被修改,会造成全部缓存失效。
在java层面,就是对多个相邻的变量都加了volatile,而且这些变量还被不同的线程使用。
private volatile int a;
private volatile int b;
private volatile int c;
thread1 {
a=1;
}
thread2 {
b=2;
}
thread3 {
c=2;
}
如何避免
自己注意,不要写一大堆volatile在一起,即使写一起,也是共同用于一个事务操作。
或者在volatile变量之间,穿插其他普通变量
增加@Contended注解,需要开启-XX:-RestrictContended