缓存一致性协议与伪共享问题

什么是缓存行?

在cpu进行读取数据的时候,会向内存中拿数据,而cpu和内存读取数据的速度差别很大,所有加入了缓存进行了缓冲,大大提高了cpu从内存拿去数据的速度。缓存又分一级缓存、二级缓存、三级缓存,读取数据的速度依次降低,而这些缓存存储数据的基本单位就是缓存行。

通常情况下,存储器价格会随着速度的上升而上升,因为高速存储器的成本十分高昂。比如寄存器最快,价格也最贵;内存慢许多,价格也便宜很多。而寄存器与内存的速度差异十分巨大,因此CPU就引入了速度和价格都在二者中间的cpu缓存来平衡这种差异。

当CPU尝试访问某个变量时,会先在一级中查找,如果命中缓存则直接读取数据;如果没有找到,就去下一级,一直到内存,随后将该变量所在的一个缓存行大小的区域复制到缓存中。查找的路线最长,速度越慢,因此将那些访问频繁的数据应当保存在一级缓存中。另外,一个变量的大小往往小于一个缓存行的大小,这时就有可能把内存中和当前变量连续的地址一起读进缓存行(一般缓存行占64个字节)。

缓存一致性原则

多核CPU的情况下,每个核都会有一个独立的一级二级缓存。如果一个变量A被多个cpu处理器读取了数据(一个cpu几核就是几个处理器),然后存储在缓存中,当其中任意一个处理器修改了变量A,那么其他处理器存储变量A所在的缓存行失效,如果其他处理器要操作变量A,就要重新取内存中读取数据。

缓存行的优势

在单线程中,缓存行大大提高了cpu访问内存的速度,比如java遍历数据,如果按顺序遍历如以下代码只需要用时120ms。


  
  
  1. public class Test7_psuado_share {
  2. static final int LINE_NUM = 10240;
  3. static final int COLUM_NUM = 10240;
  4. public static void main(String[] args) {
  5. long[][] array = new long[LINE_NUM][COLUM_NUM];
  6. long startTime = System. currentTimeMillis();
  7. //方案一:执行时间120ms
  8. //多个地址连续的变量才有可能被放到同一个缓存行中,读取数组第一个元素时,多个元素会被放到同一个缓存行
  9. //这样顺序访问数组元素时会在缓存行中直接命中,就不会到内存中读取了
  10. for ( int i = 0; i < LINE_NUM; i++) {
  11. for ( int j = 0; j < COLUM_NUM; j++) {
  12. array[i][j] = i * 2 + j;
  13. }
  14. }
  15. System.out. println(System. currentTimeMillis() - startTime);
  16. }
  17. }

而如果我们按列遍历数组,也就是地址不连续的跳动遍历 ,如以下代码,耗时2284ms。


  
  
  1. public class Test7_psuado_share {
  2. static final int LINE_NUM = 10240;
  3. static final int COLUM_NUM = 10240;
  4. public static void main(String[] args) {
  5. long[][] array = new long[LINE_NUM][COLUM_NUM];
  6. long startTime = System. currentTimeMillis();
  7. // 方案二:执行时间2284ms
  8. // 方案二是跳跃式的,破坏了程序访问的局部性原则,且缓存容量有限
  9. // 当缓存满了时会根据一定的淘汰算法替换缓存行,从而降低程序性能
  10. for ( int i = 0; i < LINE_NUM; i++) {
  11. for ( int j = 0; j < COLUM_NUM; j++) {
  12. array[j][i] = i * 2 + j;
  13. }
  14. }
  15. System.out. println(System. currentTimeMillis() - startTime);
  16. }
  17. }

很明显缓存带来的效率大大提高了20倍,如上面所说,如果cpu读取long型数组第一个下标的值时, 会把之后的七个数的值全部读入一个缓存行,当继续遍历数组时,cpu直接在缓存命中,拿去数据,大大减少了取内存拿去数据的时间。

缓存行的劣势

假设我们有两个连续存放(例如数组)的int:a和b,缓存行大小是64字节,则ab可能被存放在一个缓存行中,我们的CPU有两个核心,恰好这两个核心分别在读写这a和b。此时会发生什么情况?

处理器1 修改了a,导致处理器2存在a的缓存行失效,而ab在一起,也就导致了b的失效,当处理器2要去操作b数据要重新取内存中读取数据,导致了缓存的加入没起到作用。

处理器2 修改了b,导致处理器1存在b的缓存行失效,而ab在一起,也就导致了a的失效,处理器1要去操作数据b要重新取内存读取数据

这种因为一个缓存行缓存多个变量,而多个核心又在修改一个缓存上的多个变量时引发的缓存失效问题,这种问题就是伪共享。

如果避免伪共享?

避免伪共享的方式往往是进行填充,例如a和b原本可以放到同一个缓存行中,在a的后面填充一些内容,使得a和b无法同时放下,这样来a和b被放到同一个缓存行中。当然,进行填充会导致缓存行大量空间的浪费,因此应当权衡场景,采取正确的策略

代码演示

代码由N个线程分别频繁访问数组的0-N-1位置的元素

VolatileLong中只有一个被volatile修饰的long变量,多个Volatile Long可能被放在一个缓存行中。

Volatile Long2中的变量前后都有padding变量,多个VolatileLong2不在一个缓存行


  
  
  1. public class Main {
  2. private final static int CORE_COUNT = Runtime. getRuntime(). availableProcessors();
  3. private final static long LOOP = 100 * 1000 * 1000L;
  4. // 6729ms 伪共享
  5. // private static VolatileLong[] longs = new VolatileLong[CORE_COUNT];
  6. // 1460ms 填充解决伪共享问题
  7. private static VolatileLong2[] longs = new VolatileLong2[CORE_COUNT];
  8. public static void main(final String[] args) throws Exception {
  9. for ( int i = 0; i < longs.length; i++) {
  10. longs[i] = new VolatileLong2();
  11. }
  12. Thread[] threads = new Thread[Runtime. getRuntime(). availableProcessors()];
  13. for ( int i = 0; i < threads.length; i++) {
  14. threads[i] = new Thread( new Test(i));
  15. }
  16. long startTime = System. currentTimeMillis();
  17. for (Thread thread : threads) {
  18. thread. start();
  19. }
  20. for (Thread thread : threads) {
  21. thread. join();
  22. }
  23. System.out. println(System. currentTimeMillis() - startTime);
  24. }
  25. static class Test implements Runnable {
  26. private int index = 0;
  27. public Test(int index) {
  28. this.index = index;
  29. }
  30. @ Override
  31. public void run () {
  32. long i = LOOP;
  33. while ( 0 != --i) {
  34. longs[index].value = i;
  35. }
  36. }
  37. }
  38. static class VolatileLong {
  39. public volatile long value = 0L;
  40. }
  41. static class VolatileLong2 {
  42. volatile long i0, i1, i2, i3, i4, i5, i6;
  43. public volatile long value = 0L;
  44. volatile long j0, j1, j2, j3, j4, j5, j6;
  45. }
  46. }

JDK8之后,提供了一个@Contended注解,使用它就可以进行自动填充


  
  
  1. /** The current seed for a ThreadLocalRandom */
  2. @sun.misc.Contended("tlr")
  3. long threadLocalRandomSeed;
  4. /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
  5. @sun.misc.Contended("tlr")
  6. int threadLocalRandomProbe;
  7. /** Secondary seed isolated from public ThreadLocalRandom sequence */
  8. @sun.misc.Contended("tlr")
  9. int threadLocalRandomSecondarySeed;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值