并发编程的基础知识(二)

1 什么是伪共享

为了解决计算机系统中主内存与CPU之间运行速度差的问题,会在CPU与主内存之间添加一级或者多级高速缓冲存储器(Cache)。这个Cache一般是被集成到CPU内部的,所以也称为CPU Cache。图1-1是两级Cache结构示意图。
两级Cache结构
在Cache内部是按行进行存储的,其中每一行称为一个Cache行。Cache行是Cache与主内存进行数据交换的单位,Cache行的大小一般为2的幂次方字节。Cache如下图所示。
Cache
当CPU访问某个变量时,首先会去看Cpu Cache内是否有该变量,如果有则直接从里面获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。由于存放到Cache行的是内存块,而不是单个变量,所以可能会把多个变量存放到一个Cache行里面。当多个变量同时修改同一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。

图1-3是伪共享示意图,下面来解析一下这个伪共享的流程。
伪共享示意图

  • 变量X和Y同时被放到了CPU的一级和二级缓存
  • 当线程1使用CPU1对变量X进行更新时,首先会修改CPU1的一级缓存变量X所在的缓存行,这时在缓存一致性协议(MESI协议1)下,CPU2中变量X对应的缓存航失效。
  • 线程2在写入变量Y时就只能去二级缓存查找,这就破坏了一级缓存。一级缓存比二级缓存更快,这也说明了多个线程不可能同时去修改自己所使用的CPU中相同缓存行里面的变量。
  • 如果CPU只有一级缓存,则会导致频繁地访问主存。

2 为何会出现伪共享

伪共享的产生是因为多个变量被放入了一个缓存行里面,并且多个线程同时去写入缓存行中不同的变量。因为缓存行是Cache与内存交换的基本单位,当CPU要访问的变量没有在缓存行中找到时,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。缓存行的大小一般是64个字节。

long a,b,c,d,e,f,g,h;

上述代码声明了8个变量,当CPU访问a的时候,发现该变量没有在缓存中,就会去主内存把a以及内存地址附近的b、c、d、e、f、g、h放入缓存行。也就是地址连续的多个变量才有可能会放到一个缓存行中。

3 如何避免伪共享

3.1 JDK8之前的解决方案

在JDK8之前,一般都是通过字节填充的方式来避免该问题。就是创建一个变量的时候,使用填充字段填充该变量所在的缓存行。

   // jdk7以上使用此方法
   public final static class VolatileLong
    {
        public volatile long value = 0L;
        public long p1, p2, p3, p4, p5, p6;
    }

计算一下,是不是满足64字节?
long类型变量占用8个字节,这里一共7个long类型的变量,总共56字节。VolatileLong是一个类对象,类对象的字节码的对象头占用8个字节,正好64个字节。

3.2 JDK8的解决方案

Java8中已经提供了官方的解决方案,Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。

   @sun.misc.Contended
   public final static class VolatileLong
    {
        public volatile long value = 0L;
        public long p1, p2, p3, p4, p5, p6;
    }

  1. 在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是:
    M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中;
    E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中;
    S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中;
    I(Invalid):这行数据无效,数据被修改了,和内存中的数据不一致,除去M状态所在的缓存行外,其余的缓存行都失效。 ↩︎

发布了16 篇原创文章 · 获赞 5 · 访问量 3295

猜你喜欢

转载自blog.csdn.net/qq_32573109/article/details/101363021