每个线程只管自己的变量,性能却不如单线程?问题出在缓存行 _
伪共享False Sharing是多线程编程中一个很容易被忽略但在高并发场景下又可能非常致命的性能问题。它最迷惑人的地方在于从业务代码上看多个线程并没有修改同一个变量甚至每个线程都只操作属于自己的那份数据理论上不应该发生竞争但从 CPU 的视角看这些变量可能刚好落在同一个缓存行里于是一个线程修改自己的变量时会导致其他 CPU 核心上的缓存行失效最终引发大量无意义的缓存同步。所以伪共享不是“逻辑共享”导致的问题而是“物理存储位置太近”导致的问题。本文内容从现代处理器的缓存结构说起缓存行为什么是 CPU 缓存的基本单位什么是 CPU 缓存一致性为什么缓存一致性会引出伪共享问题用 Java 代码演示伪共享和缓存行填充伪共享的常见解决方案实际项目中该如何判断和取舍CPU为什么需要缓存在理解伪共享之前我们要先理解一个基础问题CPU 为什么需要缓存现代 CPU 的执行速度非常快而内存相对 CPU 来说要慢很多。如果每一次读取变量、写入变量都直接访问主内存那么 CPU 大部分时间都会浪费在等待内存数据返回上。为了缓解这个问题CPU 和主内存之间会加入多级缓存也就是我们常说的 L1、L2、L3 Cache。一般来说缓存层级可以简单理解为L1 Cache离 CPU 核心最近速度最快容量最小通常每个核心独享L2 Cache速度比 L1 慢一些容量比 L1 大一些很多处理器中也是每个核心独享L3 Cache速度再慢一些但容量更大通常多个核心共享主内存容量最大但访问延迟远高于 CPU Cache也就是说一个变量并不是每次都从内存中直接读取。CPU 会尽量把最近访问过的数据放到缓存里下次再访问相同数据或相邻数据时就可以直接从缓存中拿到速度会快很多。这背后依赖两个很重要的局部性原理时间局部性一个数据刚被访问过后续很可能还会再次被访问空间局部性一个数据被访问时它附近的数据也很可能会被访问比如我们遍历一个数组javafor (int i 0; i arr.length; i) { sum arr[i]; }CPU 读取arr[0]时并不会只把arr[0]这几个字节加载到缓存里而是会把它附近的一整块连续内存都加载进来。这样后续访问arr[1]、arr[2]时大概率已经命中缓存不需要再去主内存读取。这个“一整块连续内存”就是接下来要讲的缓存行。缓存行在现代处理器中缓存行Cache Line是 CPU Cache 和主内存之间进行数据交换的最小单位。主流 CPU 的缓存行大小通常是 64 字节。注意这里的重点是“最小单位”。假设有一个long类型变量占 8 字节。当 CPU 需要读取这个long变量时并不是只从主内存加载 8 字节而是会把包含这个变量的一整个缓存行加载到 CPU Cache 中。如果缓存行大小是 64 字节那么一次就会加载 64 字节。比如内存中有一段连续的数据text| long a | long b | long c | long d | long e | long f | long g | long h |一个long占 8 字节8 个long正好占 64 字节。假设它们刚好处在同一个缓存行里那么 CPU 访问a时实际上会把a到h这一整段数据都加载到缓存里。这样做大多数时候是有好处的。比如遍历数组时CPU 预先加载相邻数据可以显著提升访问效率。但凡事都有两面性当多个线程在不同 CPU 核心上修改同一个缓存行里的不同变量时问题就来了。CPU缓存一致性是什么现在考虑一个多核 CPU。每个核心都有自己的缓存多个核心又共享同一块主内存。如果只有读操作一切都比较简单。多个核心都可以把同一份数据加载到各自的缓存里大家读到的值一致即可。但如果有写操作问题就复杂了。假设变量x的初始值为 1线程 A 在 CPU Core 1 上运行线程 B 在 CPU Core 2 上运行Core 1 把x 1加载到自己的缓存中Core 2 也把x 1加载到自己的缓存中线程 A 把x修改为 2线程 B 如果继续从自己的缓存中读取x是不是还会读到旧值 1为了避免不同核心看到的数据互相矛盾CPU 需要一套机制来维护缓存之间的数据一致性这就是 CPU 缓存一致性。常见的一致性协议是 MESI它把缓存行的状态大致分为下面几类状态含义Modified当前缓存行被本核心修改过数据和主内存不一致其他核心没有有效副本Exclusive当前缓存行只被本核心持有数据和主内存一致Shared当前缓存行可能被多个核心持有数据和主内存一致Invalid当前缓存行已经失效不能继续使用这里不需要把 MESI 的所有细节背下来我们只要抓住一个关键点CPU 维护一致性的单位不是某个 Java 字段也不是某个 C 语言变量而是缓存行。也就是说只要某个核心修改了一个缓存行中的任意一个字节其他核心中同一个缓存行的副本就可能被标记为失效。这句话就是理解伪共享的关键。从缓存一致性到伪共享现在我们构造一个场景。有两个线程分别运行在两个 CPU 核心上线程 A 只修改变量a线程 B 只修改变量b从业务逻辑上看a和b是两个完全不同的变量但从内存布局上看a和b刚好落在同一个缓存行中它可能长这样text同一个缓存行64字节 --------------------------------------------------------------- | a | b | 其他数据 | --------------------------------------------------------------- ^ ^ 线程A 线程B此时会发生什么线程 A 修改aCore 1 获得这个缓存行的写权限Core 2 上相同缓存行的副本被标记为 Invalid线程 B 修改b发现自己的缓存行失效只能重新加载并获得写权限Core 1 上相同缓存行的副本又被标记为 Invalid线程 A 下一次修改a又要重新加载这个缓存行两个线程明明没有修改同一个变量却在缓存行层面互相“打扰”。这种因为不同变量共享同一个缓存行而导致的无意义缓存失效就是伪共享。

相关新闻