伪共享( false sharing )

一、相关概念

        高性能异步处理框架 Disruptor,它被誉为“最快的消息框架”,其 LMAX 架构能够在一个线程里每秒处理 6百万 订单!在讲到 Disruptor 为什么这么快时,接触到了一个概念————  伪共享( false sharing )

        其中提到:缓存行上的写竞争是运行在 SMP(多对称系统) 系统中并行线程实现可伸缩性最重要的限制因素。由于从代码中很难看出是否会出现伪共享,有人将其描述成无声的性能杀手。

        伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

        缓存行:缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。

        伪共享( false sharing )

        我们可以通过缓存行填充来消除伪共享,上图是两个long类型的x变量和y变量。他们是连续存储的,变量x,y同时被放到了CPU的一级和二级缓存当线程1使用CPU1对变量x进行更新时候

首先会修改cpu1的一级缓存变量x所在缓存行,这时候缓存一致性协议会导致cpu2中变量x对应的缓存行失效那么线程2写入变量x的时候就只能去二级缓存去查找,这就破坏了一级缓存,而一级缓存比二级缓存更快。

更坏的情况下如果cpu只有一级缓存,那么会导致频繁的直接访问主内存。

 

二、为什么会产生伪共享

        伪共享的产生是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同变量。那么为何多个变量会被放入一个缓存行那。其实是因为Cache与内存交换数据的单位就是Cache,当CPU要访问的

变量没有在Cache命中时候,根据程序运行的局部性原理会把该变量在内存中大小为Cache行的内存放如缓存行。

         long a;    long b;    long c;    long d;声明了四个long变量,假设cache行的大小为32个字节,那么当cpu访问变量a时候发现该变量没有在cache命中,那么就会去主内存把变量a以及内存地址附近的b,c,d放入缓存行。

也就是地址连续的多个变量才有可能会被放到一个缓存行中,当创建数组时候,数组里面的多个元素就会被放入到同一个缓存行。

 

三、如何消伪共享

        (1)JDK8之前一般都是通过字节填充的方式来避免,也就是创建一个变量的时候使用填充字段填充该变量所在的缓存行,这样就避免了多个变量存在同一个缓存行。

            public final static class FilledLong {
            public volatile long value = 0L;
            public long p1, p2, p3, p4, p5, p6;    
        }

        假如Cache行为64个字节,那么我们在FilledLong类里面填充了6个long类型变量,每个long类型占用8个字节,加上value变量的8个字节总共56个字节,另外这里FilledLong是一个类对象,

而类对象的字节码的对象头占用了8个字节,所以当new一个FilledLong对象时候实际会占用64个字节的内存,这个正好可以放入Cache的一个行。

         (2)在JDK8中提供了一个sun.misc.Contended注解,用来解决伪共享问题,上面代码可以修改为如下:

         @sun.misc.Contended 
         public final static class FilledLong {
            public volatile long value = 0L;
        }

        (3)上面是修饰类的,当然也可以修饰变量,比如Thread类中的使用:


        @sun.misc.Contended("tlr")
        long threadLocalRandomSeed;


        @sun.misc.Contended("tlr")
        int threadLocalRandomProbe;


        @sun.misc.Contended("tlr")
        int threadLocalRandomSecondarySeed;