0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

高性能缓存设计:如何解决缓存伪共享问题

京东云 来源:jf_75140285 作者:jf_75140285 2025-07-01 15:01 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

在多核高并发场景下,缓存伪共享(False Sharing) 是导致性能骤降的“隐形杀手”。当不同线程频繁修改同一缓存行(Cache Line)中的独立变量时,CPU缓存一致性协议会强制同步整个缓存行,引发无效化风暴,使看似无关的变量操作拖慢整体效率。本文从缓存结构原理出发,通过实验代码复现伪共享问题(耗时从3709ms优化至473ms),解析其底层机制;同时深入剖析高性能缓存库 Caffeine 如何通过 内存填充技术(120字节占位变量)隔离关键字段,以及 JDK 1.8 的 @Contended 注解如何以“空间换时间”策略高效解决伪共享问题,揭示缓存一致性优化的核心思想与实践价值,为开发者提供性能调优的关键思路。

伪共享

伪共享(False sharing)是一种会导致性能下降的使用模式,最常见于现代多处理器CPU缓存中。当不同线程频繁修改同一缓存行(Cache Line)中不同变量时,由于CPU缓存一致性协议(如MESI)会强制同步整个缓存行,导致线程间无实际数据竞争的逻辑变量被迫触发缓存行无效化(Invalidation),引发频繁的内存访问和性能下降。尽管这些变量在代码层面彼此独立,但因物理内存布局相邻,共享同一缓存行,造成“虚假竞争”,需通过内存填充或字段隔离使其独占缓存行解决。

接下来我们讨论并验证在 CPU 缓存中是如何发生伪共享问题的,首先我们需要先介绍一下 CPU 的缓存结构,如下图所示:

wKgZPGhiYXSAWBAQAAKTcpGKIOw367.png

CPU Cache 通常分为大小不等的三级缓存,分别为 L1 Cache、L2 Cache、L3 Cache,越靠近 CPU 的缓存,速度越快,容量也越小。CPU Cache 实际上由很多个缓存行 Cache Line 组成,通常它的大小为 64 字节(或 128 字节),是 CPU 从内存中 读取数据的基本单位,如果访问一个 long[] 数组,当其中一个值被加载到缓存中时,它会额外加载另外 7 个元素到缓存中。那么我们考虑这样一种情况,CPU 的两个核心分别访问和修改统一缓存行中的数据,如下图所示:

wKgZO2hiYXWAX1oFAAI12kQOkt8607.png

核心 1 不断地访问和更新值 X,核心 2 则不断地访问和更新值 Y,事实上每当有核心对某一缓存行中的数据进行修改时,都会导致其他核心的缓存行失效,从而导致其他核心需要重新加载缓存行数据,进而导致性能下降,这也就是我们上文中所说的缓存伪共享问题。接下来我们用一段代码来验证下缓存伪共享问题造成的性能损失,如下所示:

public class TestFalseSharing {

    static class Pointer {
        // 两个 volatile 变量,保证可见性
        volatile long x;
        volatile long y;

        @Override
        public String toString() {
            return "x=" + x + ", y=" + y;
        }
    }

    @Test
    public void testFalseSharing() throws InterruptedException {
        Pointer pointer = new Pointer();

        // 启动两个线程,分别对 x 和 y 进行自增 1亿 次的操作
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100_000_000; i++) {
                pointer.x++;
            }
        });
        Thread t2 = new Thread(() - > {
            for (int i = 0; i < 100_000_000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }

}

这种情况下会发生缓存的伪共享,x 和 y 被加载到同一缓存行中,当其中一个值被修改时,会使另一个核心中的该缓存行失效并重新加载,代码执行实际耗时为 3709ms。如果我们将 x 变量后再添加上 7 个 long 型的元素,使得变量 x 和变量 y 分配到不同的缓存行中,那么理论上性能将得到提升,我们实验一下:

public class TestFalseSharing {

    static class Pointer {
        volatile long x;
        long p1, p2, p3, p4, p5, p6, p7;
        volatile long y;

        @Override
        public String toString() {
            return "x=" + x + ", y=" + y;
        }
    }

    @Test
    public void testFalseSharing() throws InterruptedException {
        // ...
    }

}

本次任务执行耗时为 473ms,性能得到了极大的提升。现在我们已经清楚的了解了缓存伪共享问题,接下来我们讨论下在 Caffeine 中是如何解决缓存伪共享问题的。

Caffeine 对缓存伪共享问题的解决方案

在 缓存之美:万文详解 Caffeine 实现原理 中我们提到过,负责记录写后任务的 WriterBuffer 数据结构的类继承关系如下所示:

wKgZPGhiYXaAPMq1AArX0SqWOsQ148.png

如图中标红的类所示,它们都是用来解决伪共享问题的,我们以 BaseMpscLinkedArrayQueuePad1 为例来看下它的实现:

abstract class BaseMpscLinkedArrayQueuePad1< E > extends AbstractQueue< E > {
    byte p000, p001, p002, p003, p004, p005, p006, p007;
    byte p008, p009, p010, p011, p012, p013, p014, p015;
    byte p016, p017, p018, p019, p020, p021, p022, p023;
    byte p024, p025, p026, p027, p028, p029, p030, p031;
    byte p032, p033, p034, p035, p036, p037, p038, p039;
    byte p040, p041, p042, p043, p044, p045, p046, p047;
    byte p048, p049, p050, p051, p052, p053, p054, p055;
    byte p056, p057, p058, p059, p060, p061, p062, p063;
    byte p064, p065, p066, p067, p068, p069, p070, p071;
    byte p072, p073, p074, p075, p076, p077, p078, p079;
    byte p080, p081, p082, p083, p084, p085, p086, p087;
    byte p088, p089, p090, p091, p092, p093, p094, p095;
    byte p096, p097, p098, p099, p100, p101, p102, p103;
    byte p104, p105, p106, p107, p108, p109, p110, p111;
    byte p112, p113, p114, p115, p116, p117, p118, p119;
}

abstract class BaseMpscLinkedArrayQueueProducerFields< E > extends BaseMpscLinkedArrayQueuePad1< E > {
    // 生产者操作索引(并不对应缓冲区 producerBuffer 中索引位置)
    protected long producerIndex;
}

可以发现在这个类中定义了 120 个字节变量,这样缓存行大小不论是 64 字节还是 128 字节,都能保证字段间的隔离。如图中所示 AbstractQueue 和 BaseMpscLinkedArrayQueueProducerFields 中的变量一定会 被分配到不同的缓存行 中。同理,借助 BaseMpscLinkedArrayQueuePad2 中的 120 个字节变量,BaseMpscLinkedArrayQueueProducerFields 和 BaseMpscLinkedArrayQueueConsumerFields 中的变量也会被分配到不同的缓存行中,这样就避免了缓存的伪共享问题。

其实除了 Caffeine 中有解决缓存伪共享问题的方案外,在 JDK 1.8 中引入了 @Contended 注解,它也可以解决缓存伪共享问题,如下所示为它在 ConcurrentHashMap 中的应用:

public class ConcurrentHashMap< K,V > extends AbstractMap< K,V >
        implements ConcurrentMap< K,V >, Serializable {
    // ...
    
    @sun.misc.Contended
    static final class CounterCell {
        volatile long value;

        CounterCell(long x) {
            value = x;
        }
    }
}

其中的内部类 CounterCell 被标记了 @sun.misc.Contended 注解,表示该类中的字段会与其他类的字段相隔离,如果类中有多个字段,实际上该类中的变量间是不隔离的,这些字段可能被分配到同一缓存行中。因为 CounterCell 中只有一个字段,所以它会被被分配到一个缓存行中,剩余缓存行容量被空白内存填充,本质上也是一种以空间换时间的策略。这样其他变量的变更就不会影响到 CounterCell 中的变量了,从而避免了缓存伪共享问题。

这个注解不仅能标记在类上,还能标记在字段上,拿我们的的代码来举例:

public class TestFalseSharing {

    static class Pointer {
        @Contended("cacheLine1")
        volatile long x;
        //        long p1, p2, p3, p4, p5, p6, p7;
        @Contended("cacheLine2")
        volatile long y;

        @Override
        public String toString() {
            return "x=" + x + ", y=" + y;
        }
    }
    
    @Test
    public void testFalseSharing() throws InterruptedException {
        // ...
    }

}

它可以指定内容来 定义多个字段间的隔离关系。我们使用注解将这两个字段定义在两个不同的缓存行中,执行结果耗时与显示声明字段占位耗时相差不大,为 520ms。另外需要注意的是,要想使注解 Contended 生效,需要添加 JVM 参数 -XX:-RestrictContended。

再谈伪共享

避免伪共享的主要方法是代码检查,而且伪共享可能不太容易被识别出来,因为只有在线程访问的是不同且碰巧在主存中相邻的全局变量时才会出现伪共享问题,线程的局部存储或者局部变量不会是伪共享的来源。此外,解决伪共享问题的本质是以空间换时间,所以并不适用于在大范围内解决该问题,否则会造成大量的内存浪费。

巨人的肩膀

维基百科 - 伪共享

小林coding - 2.3 如何写出让 CPU 跑得更快的代码

知乎 - 杂谈 什么是伪共享(false sharing)

博客园 - CPU Cache 与缓存行

博客园 - 伪共享(false sharing),并发编程无声的性能杀手

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11382

    浏览量

    226562
  • 缓存
    +关注

    关注

    1

    文章

    248

    浏览量

    27862
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    凯芯 CSS6404LS-LI 高性能 QSPI pSRAM 消费电子终端缓存技术解析

    数据流密集,同时整机结构紧凑、成本约束严格,对配套缓存芯片的总线传输性能、访问延迟、外围电路复杂度、封装尺寸以及电气兼容性均提出严苛选型要求。现阶段消费电子硬件正
    的头像 发表于 04-21 16:08 199次阅读
    凯芯 CSS6404LS-LI <b class='flag-5'>高性能</b> QSPI pSRAM 消费电子终端<b class='flag-5'>缓存</b>技术解析

    京东缓存中间件架构与缓存内核优化

    一、京东缓存中间件架构 1、背景 在当今高并发、分布式的系统架构中,缓存已成为提升应用性能、降低数据库负载的核心组件。随着业务规模的扩大与系统复杂度的增加,缓存的使用和管理面临诸多挑战
    的头像 发表于 04-03 16:18 1926次阅读
    京东<b class='flag-5'>缓存</b>中间件架构与<b class='flag-5'>缓存</b>内核优化

    KeepAlive:组件缓存实现深度解析

    我们学习了 Suspense 如何处理异步组件加载。今天,我们将探索Vue3中另一个强大的特性:KeepAlive。它允许我们在组件切换时缓存组件实例,避免重复渲染,极大地提升了用户体验和性能
    发表于 03-05 19:17

    探秘DS2731:缓存内存电池备份管理IC的卓越性能与应用

    探秘DS2731:缓存内存电池备份管理IC的卓越性能与应用 在电子设备的设计中,电源管理是一个至关重要的环节,尤其是对于需要可靠备份电源的应用场景。今天,我们就来深入探讨一款功能强大的缓存内存电池
    的头像 发表于 02-24 16:40 471次阅读

    DRAM缓存真有那么重要吗?天硕工业级SSD固态硬盘实测告诉你答案!

    在国产固态硬盘推荐榜中,推荐五花八门,很多用户疑惑DRAM 缓存 SSD 值得买吗?不同方案的国产 SSD 性能对比差距到底有多大?价格差距不小,性能也各说各的好。要弄清两者的实际差异,先看国产 SSD
    的头像 发表于 01-19 16:49 541次阅读

    C语言的缓冲区(缓存)详解

    缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。   缓冲区根据其对应的是输入设备还是输出设备
    发表于 01-14 07:30

    数据全复用高性能池化层设计思路分享

    大家好,本团队此次分享的内容为可实现数据全复用高性能池化层设计思路,核心部分主要由以下3个部分组成; 1.SRAM读取模块;——池化使用的存储为SRAM 基于SRAM读与写时序,约束池化模块读与写
    发表于 10-29 07:10

    串口DMA发送有缓存吗?

    串口DMA发送有缓存吗, 我是从ringbuffer取出来,放到申请的缓存里,启动串口DMA发送,然后就释放了。暂时没发现什么问题。 用的drv_usart.c是这个版本
    发表于 10-10 06:14

    请问M453是否默认启用4kb缓存

    浏览 M453 的各种示例代码,我没有看到显式启用 4kb 缓存,那么缓存是否默认启用?
    发表于 08-28 08:27

    Redis缓存的经典问题和解决方案

    用户疯狂查询数据库中不存在的数据,每次查询都绕过缓存直接打到数据库,导致数据库压力骤增。
    的头像 发表于 08-20 16:24 976次阅读

    最新版K230IDE怎么打开帧缓存区?

    最新版K230IDE怎么打开帧缓存
    发表于 08-08 06:01

    缓存之美:万文详解 Caffeine 实现原理(上)

    用于统计元素访问频率的 Count-Min Sketch 数据结构、理解内存屏障和如何避免缓存共享问题、MPSC 多线程设计模式、高性能缓存
    的头像 发表于 08-05 14:49 880次阅读
    <b class='flag-5'>缓存</b>之美:万文详解 Caffeine 实现原理(上)

    本地缓存 Caffeine 中的时间轮(TimeWheel)是什么?

    我们详细介绍了 Caffeine 缓存添加元素和读取元素的流程,并详细解析了配置固定元素数量驱逐策略的实现原理。在本文中我们将主要介绍 配置元素过期时间策略的实现原理 ,补全 Caffeine
    的头像 发表于 08-05 14:48 758次阅读
    本地<b class='flag-5'>缓存</b> Caffeine 中的时间轮(TimeWheel)是什么?

    harmony-utils之CacheUtil,缓存工具类

    harmony-utils之CacheUtil,缓存工具类
    的头像 发表于 07-04 16:36 671次阅读

    请问如何在C++中使用NPU上的模型缓存

    无法确定如何在 C++ 中的 NPU 上使用模型缓存
    发表于 06-24 07:25