The image of false sharing

1. 背景:CPU 缓存与 Cache Line

  • CPU 缓存是为了加速内存访问引入的高速存储。
  • 缓存不是一字节一字节加载,而是以 缓存行(Cache Line) 为单位,常见大小是 64 字节
  • 当 CPU 访问某个变量时,整个缓存行(包含这个变量周围的数据)都会被加载到 CPU 缓存中。

2. 什么是伪共享?

伪共享是指:

  • 两个或多个 逻辑上互不相关 的变量(例如队列的 headtail 指针),在 物理内存上相邻,从而落在同一个缓存行里。
  • 如果不同线程分别操作这些变量,由于缓存一致性协议(MESI 等)的存在:

    • 一个线程修改 a,会让整个缓存行失效;
    • 另一个线程即使只读/写 b,也必须重新从内存同步缓存行。
  • 结果就是 不必要的缓存行失效与同步开销,极大影响多线程程序的性能。

📌 关键词:共享缓存行,但逻辑上不该共享 —— 所以叫 “伪共享”。

3. 举个例子

假设缓存行大小为 64 字节:

class Example {
    volatile long a;  // 8 字节
    volatile long b;  // 8 字节
}
  • ab 在内存上是连续的(共占 16 字节)。
  • 如果它们被不同线程频繁修改,就会落在同一个缓存行。
  • 即使线程 1 只改 a,线程 2 只改 b,它们也会互相干扰(缓存行失效)。

4. 如何解决伪共享

常见方法是 填充(padding),让不同变量不落在同一缓存行:

(1) 手工填充字段

class Example {
    volatile long a;
    long p1, p2, p3, p4, p5, p6, p7;  // 填充,防止 b 与 a 同一行
    volatile long b;
}

(2) 使用注解 @sun.misc.Contended(JDK 8+)*

@sun.misc.Contended
class Example {
    volatile long a;
    volatile long b;
}
  • @Contended 会在字段前后自动加上填充字节,保证它们分离在不同缓存行。
  • 需要 JVM 参数 -XX:-RestrictContended 启用。

⚠️ 警告:此方法在高版本 JDK 中已过时(我测试使用的是 OpenJDK 24),不能再使用了, 即使使用 @jdk.internal.vm.annotation.Contended (JDK 9+)也不行,这也是内部 API, 官方不推荐在生产环境中直接使用。建议使用第一种手动填充或第三方库(JCTools)

5. 什么时候会遇到伪共享?

  • 高并发队列的 head / tail 指针(生产者消费者模型)。
  • 计数器数组(例如统计请求数的场景)。
  • 自旋锁、原子变量等高频修改的共享数据结构。

6. 总结

  • 伪共享不是“共享数据”,而是“共享缓存行” 导致的问题。
  • 本质原因:缓存一致性协议 + 内存排布紧密。
  • 解决思路:避免热点变量处于同一个缓存行(padding 或 @Contended)。


通过以下带伪共享和去伪共享对比的 Java Benchmark 代码(JMH),让我们直观看看性能差距:

JMH 基准测试代码

创建一个 Maven 项目,并添加 JMH 依赖。

1. Maven 依赖 (pom.xml)

<project ...>
    <properties>
        <maven.compiler.source>24</maven.compiler.source>
        <maven.compiler.target>24</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.37</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.37</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

2. Java Benchmark 代码

这个类包含了两种情况的测试:

  • StateWithFalseSharing: 两个 volatile long 变量紧挨着,极易产生伪共享。
  • StateWithoutFalseSharing: 在两个 volatile long 变量之间填充了 15 个 long 作为“垫料”,避免了伪共享。
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.ThreadParams;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput) // 测试模式:吞吐量 (每秒操作数)
@OutputTimeUnit(TimeUnit.SECONDS) // 输出结果的时间单位:秒
@Warmup(iterations = 5, time = 1) // 预热:5轮,每轮1秒
@Measurement(iterations = 5, time = 1) // 测量:5轮,每轮1秒
@Fork(1) // Fork 出一个进程进行测试
@State(Scope.Group) // 状态的作用域,Group 作用域保证同一组的线程共享实例
public class FalseSharingBenchmark {

    /**
     * 存在伪共享问题的状态类
     *
     * value1 和 value2 很可能在同一个缓存行 (64 字节) 中
     * 因为 long 是 8 字节,两个 long 是 16 字节,远小于 64 字节
     * 添加 @State 注解,Scope.Group 表示同一组的线程共享此实例
     */
    @State(Scope.Group)
    public static class StateWithFalseSharing {
        public volatile long value1 = 0;
        public volatile long value2 = 0;
    }

    /**
     * 通过缓存行填充解决了伪共享问题的状态类
     */
    @State(Scope.Group)
    public static class StateWithoutFalseSharing {
        public volatile long value1 = 0;

        // 填充 7 个 long 再加上前一个 long (7 * 8 + 1 * 8 = 64 字节),超过一个缓存行的大小(通常一个缓存行是 64 字节)
        // 确保 value1 和 value2 落在不同的缓存行
        private long p1, p2, p3, p4, p5, p6, p7;

        public volatile long value2 = 0;
    }

    // --------------- 基准测试方法 ---------------

    @Benchmark
    @Group("FalseSharing") // 将两个线程的操作归为一组
    @GroupThreads(2) // 使用两个线程
    public void testFalseSharing(StateWithFalseSharing state, ThreadParams threadParams) {
        // 根据线程索引,让两个线程分别修改不同的变量
        if (threadParams.getThreadIndex() == 0) {
            state.value1++;
        } else {
            state.value2++;
        }
    }

    @Benchmark
    @Group("NoFalseSharing") // 将两个线程的操作归为另一组
    @GroupThreads(2) // 同样使用两个线程
    public void testNoFalseSharing(StateWithoutFalseSharing state, ThreadParams threadParams) {
        // 根据线程索引,让两个线程分别修改不同的变量
        if (threadParams.getThreadIndex() == 0) {
            state.value1++;
        } else {
            state.value2++;
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(FalseSharingBenchmark.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }
}

如何运行

  1. 将上述代码保存到你的 Maven 项目的 src/main/java 目录下。
  2. 使用 Maven 构建项目:mvn clean package
  3. 可以直接在 IDE 中运行 main 方法,或者执行打包后的 jar 文件。

预期结果分析

当你运行这个基准测试后,你会得到类似下面的输出(具体数值取决于你的机器配置,以下是我在 M1 Pro Mac 下得到的输出, 非伪共享版比伪共享版性能高 4 倍以上):

Benchmark                              Mode  Cnt          Score          Error  Units
FalseSharingBenchmark.FalseSharing    thrpt    5   75292908.335 ± 17920363.242  ops/s
FalseSharingBenchmark.NoFalseSharing  thrpt    5  329029833.864 ±  1406771.525  ops/s

结果解读:

  • FalseSharing: 吞吐量(Score)非常低。这是因为两个线程在不断地修改位于同一缓存行的 value1value2。线程 A 修改 value1,导致线程 B 所在的 CPU 核心的缓存行失效;紧接着线程 B 修改 value2,又导致线程 A 的缓存行失效。这种来回的“缓存同步”严重拖慢了执行速度。
  • NoFalseSharing: 吞吐量非常高,可能是前者的4 到 10 倍甚至更高。这是因为通过填充字节,value1value2 被强制分配到了不同的缓存行。线程 A 修改 value1 不会影响线程 B 缓存的 value2,反之亦然。两个线程可以真正地并行执行,性能自然大幅提升。

这个 Benchmark 非常直观地量化了伪共享带来的性能惩罚,以及通过简单的内存布局优化(去伪共享)所能获得的巨大性能收益。