Skip to content

性能微优化-缓存伪共享

About 1226 wordsAbout 4 min

性能新书

2026-08-20

6.13 缓存伪共享

现代计算机,CPU的运行速度与从内存加载数据速度有着较大鸿沟,比如CPU从寄存器加载数据需要1纳秒时间,而从内存中需要100纳秒。因此CPU使用缓存技术,预先加载可能访问的数据到CPU的缓存里以提高性能。这些数据可能是

  • 上一次访问的数据,推测下一次可能会接着访问,保留在CPU缓存里
  • 连续存储的数据,如数组,或者对象的各个字段一次性加载到缓存

CPU是通过缓存行Cache Line作为最小操作单位,缓存行通常64字节或者128字节大小。对于如下类,变量count和index有可能同时被加载到缓存行

public static class DataWithoutPadding {
    volatile long count;
    volatile long index;
}

或者DataWithoutPadding[]中的连续存储的2个元素,类似如下

在多CPU架构下,多个CPU都缓存了DataWithoutPadding[0],当一个CPU修改DataWithoutPadding[0].count时候,其他CPU的缓存DataWithoutPadding[0].count将被通知为无效,如果需要操作count,则需要再从主内存中加载最新值,相比于直接从CPU缓存取值,性能降低了1-2各数量级

即使其他CPU没有操作count,操作的是缓存行中的index,但因为CPU是以Cache Line为最小操作单位,实际上其他CPU缓存中的index仍然被通知无效,还是需要从主存中加载

关键字volatile保证了其值的多CPU的可见性

伪共享问题是因为缓存行的某个缓存无效(如count),导致这个缓存行的其他缓存值(index)在其他CPU中变得无效

解决这个伪共享问题通常使用填充技术,保证当前64字节的缓存行只有count变量或者只有index变量。如下图

这样,当CPU0的index修改的时候,CPU1的缓存行因为没有index变量,只有count变量,可以使得count变量在缓存中被CPU1使用。而不必从主内存加载。

早期填充方法是在同一个类里,申明无意义的多个变量,比如对于count,可以前后填充

/*
https://developer.aliyun.com/article/1727112#1
*/
public class ManualPaddingLong {
    /**
     * 前填充:7个long类型变量,共56字节,避免目标变量与对象头、前序变量共享缓存行
     */
    private volatile long p1, p2, p3, p4, p5, p6, p7;
    /**
     * 目标变量,被多线程频繁修改的热点变量
     */
    public volatile long count;

    /**
     * 后填充:7个long类型变量,共56字节,避免目标变量与后序变量共享缓存行,同时应对CPU相邻缓存行预取
     */
    private volatile long q1, q2, q3, q4, q5, q6, q7;

    /**
     * 访问填充字段,避免JIT编译器将其优化消除
     * @return 填充字段的求和结果,无实际业务意义
     */
    public long avoidJitOptimize() {
        return p1 + p2 + p3 + p4 + p5 + p6 + p7 + q1 + q2 + q3 + q4 + q5 + q6 + q7;
    }
}

Java8后,可以使用@Contended,Java将在运行时候自动完成填充,@Contended可以应用在字段或者整个类上,Java高并发代码,如LongAdder.Cell,ConcurrentHashMap.CounterCell 使用此注解完成自动填充.如下是CounterCell实现

@jdk.internal.vm.annotation.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

本书例子CPUCacheLIneTest 比较了使用Contended的性能,

@jdk.internal.vm.annotation.Contended
public static class Data{
    volatile long count;
    public long addCount(){
        count++;
        return count;
    }
}

public static class DataWithoutPadding {
    volatile long count;
    public long addCount(){
        count++;
        return count;
    }

}

注意,jdk.internal.vm.annotation.Contended注解被移到了jdk.internal.vm.annotation包中,归属java.base模块,默认不对外导出,需要在编译的时候显示--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED

JMH测试代码如下

@Fork(value = 1, jvmArgsPrepend = {
        "-XX:-RestrictContended"
})
public class CPUCacheLIneTest2 {
    //连续存储
    Data[] datas = new Data[]{new Data(),new Data()};
    DataWithoutPadding[] dataWithoutPaddings = new DataWithoutPadding[]{new DataWithoutPadding(),new DataWithoutPadding()};
    @Benchmark
    @Group("fill")
    @GroupThreads(1)
    public  long  dataCount(){
        return datas[0].addCount();
    }
    @Group("fill")
    @GroupThreads(1)
    @Benchmark
    public  long  dataCount2(){
        return datas[1].addCount();
    }


    @Group("unfill")
    @GroupThreads(1)
    @Benchmark
    public  long  dataCountNoPadding(){
        return dataWithoutPaddings[0].addCount();

    }
    @Group("unfill")
    @GroupThreads(1)
    @Benchmark
    public  long  dataCountNoPadding2(){
        return dataWithoutPaddings[1].addCount();

    }
   }

这段JMH代码解释如下

注解说明
@Group对测试方法分组,本例子分为fill和unfill俩组。每组都有俩个方法调用,分别修改连续数组中的俩个值,以模拟伪共享问题
@GroupThreads(1)测试方法所在组使用的线程数,这里设定1个线程
-XX:-RestrictContendedJVM必须使用此配置,开启JVM自动填充生效。

JMH测试结果如下,可以看缓存行填充的吞吐量是2倍于无填充的场景

CPUCacheLIneTest2.fill                        thrpt   10  85953.571 ± 4483.237  ops/ms
CPUCacheLIneTest2.fill:dataCount              thrpt   10  43462.344 ± 2200.061  ops/ms
CPUCacheLIneTest2.fill:dataCount2             thrpt   10  42491.227 ± 2325.268  ops/ms
CPUCacheLIneTest2.unfill                      thrpt   10  51176.091 ± 3016.431  ops/ms
CPUCacheLIneTest2.unfill:dataCountNoPadding   thrpt   10  25595.354 ± 1539.592  ops/ms
CPUCacheLIneTest2.unfill:dataCountNoPadding2  thrpt   10  25580.737 ± 1501.565  ops/ms

关于@Contended ,请参考JEP 142: Reduce Cache Contention on Specified Fields,即“减少指定字段上的缓存争用”

知行合一