您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
高性能缓存设计:如何解决缓存伪共享问题
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
高性能缓存设计:如何解决缓存伪共享问题
wy****
2025-06-27
IP归属:北京
42浏览
在多核高并发场景下,**缓存伪共享(False Sharing)** 是导致性能骤降的“隐形杀手”。当不同线程频繁修改同一缓存行(Cache Line)中的独立变量时,CPU缓存一致性协议会强制同步整个缓存行,引发无效化风暴,使看似无关的变量操作拖慢整体效率。本文从缓存结构原理出发,通过实验代码复现伪共享问题(耗时从3709ms优化至473ms),解析其底层机制;同时深入剖析高性能缓存库 Caffeine 如何通过 **内存填充技术**(120字节占位变量)隔离关键字段,以及 JDK 1.8 的 `@Contended` 注解如何以“空间换时间”策略高效解决伪共享问题,揭示缓存一致性优化的核心思想与实践价值,为开发者提供性能调优的关键思路。 ### 伪共享 **伪共享**(False sharing)是一种会导致性能下降的使用模式,最常见于现代多处理器CPU缓存中。当不同线程频繁修改同一缓存行(Cache Line)中不同变量时,由于CPU缓存一致性协议(如MESI)会强制同步整个缓存行,导致线程间无实际数据竞争的逻辑变量被迫触发缓存行无效化(Invalidation),引发频繁的内存访问和性能下降。尽管这些变量在代码层面彼此独立,但因物理内存布局相邻,共享同一缓存行,造成“虚假竞争”,需通过内存填充或字段隔离使其独占缓存行解决。 接下来我们讨论并验证在 CPU 缓存中是如何发生伪共享问题的,首先我们需要先介绍一下 CPU 的缓存结构,如下图所示:  CPU Cache 通常分为大小不等的三级缓存,分别为 L1 Cache、L2 Cache、L3 Cache,越靠近 CPU 的缓存,速度越快,容量也越小。CPU Cache 实际上由很多个缓存行 Cache Line 组成,通常它的大小为 64 字节(或 128 字节),是 CPU 从内存中 **读取数据的基本单位**,如果访问一个 `long[]` 数组,当其中一个值被加载到缓存中时,它会额外加载另外 7 个元素到缓存中。那么我们考虑这样一种情况,CPU 的两个核心分别访问和修改统一缓存行中的数据,如下图所示:  核心 1 不断地访问和更新值 X,核心 2 则不断地访问和更新值 Y,事实上每当有核心对某一缓存行中的数据进行修改时,都会导致其他核心的缓存行失效,从而导致其他核心需要重新加载缓存行数据,进而导致性能下降,这也就是我们上文中所说的缓存伪共享问题。接下来我们用一段代码来验证下缓存伪共享问题造成的性能损失,如下所示: ```java 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 分配到不同的缓存行中,那么理论上性能将得到提升,我们实验一下: ```java 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 实现原理](http://sd.jd.com/article/42211?shareId=53471&isHideShareButton=1) 中我们提到过,负责记录写后任务的 `WriterBuffer` 数据结构的类继承关系如下所示:  如图中标红的类所示,它们都是用来解决伪共享问题的,我们以 `BaseMpscLinkedArrayQueuePad1` 为例来看下它的实现: ```java 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` 中的应用: ```java 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` 中的变量了,从而避免了缓存伪共享问题。 这个注解不仅能标记在类上,还能标记在字段上,拿我们的的代码来举例: ```java 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`。 ### 再谈伪共享 避免伪共享的主要方法是代码检查,而且伪共享可能不太容易被识别出来,因为只有在线程访问的是不同且碰巧在主存中相邻的全局变量时才会出现伪共享问题,线程的局部存储或者局部变量不会是伪共享的来源。此外,解决伪共享问题的本质是以空间换时间,所以并不适用于在大范围内解决该问题,否则会造成大量的内存浪费。 ### 巨人的肩膀 - [维基百科 - 伪共享](https://zh.wikipedia.org/wiki/%E4%BC%AA%E5%85%B1%E4%BA%AB) - [小林coding - 2.3 如何写出让 CPU 跑得更快的代码](https://www.xiaolincoding.com/os/1_hardware/how_to_make_cpu_run_faster.html) - [知乎 - 杂谈 什么是伪共享(false sharing)](https://zhuanlan.zhihu.com/p/65394173) - [博客园 - CPU Cache 与缓存行](https://www.cnblogs.com/zhongqifeng/p/14765576.html) - [博客园 - 伪共享(false sharing),并发编程无声的性能杀手](https://www.cnblogs.com/cyfonly/p/5800758.html)
上一篇:缓存之美:Guava Cache 相比于 Caffeine 差在哪里?
下一篇:WMS6.0爆品库存定位数据库并发更新行锁治理实践
wy****
文章数
42
阅读量
10987
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
缓存之美:从根上理解 ConcurrentHashMap
本文将详细介绍 ConcurrentHashMap 构造方法、添加值方法和扩容操作等源码实现。ConcurrentHashMap 是线程安全的哈希表,此哈希表的设计主要目的是在最小化更新操作对哈希表的占用,以保持并发可读性,次要目的是保持空间消耗与 HashMap 相同或更好,并支持利用多线程在空表上高效地插入初始值。在 Java 8 及之后的版本,使用 CAS 操作、 synchronized
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
42
阅读量
10987
作者其他文章
01
高性能MySQL实战(一):表结构
01
缓存之美:从根上理解 ConcurrentHashMap
01
分布式服务高可用实现:复制
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号