漫谈内存伪共享
# 什么是伪共享
在计算机组成中,CPU 的运算速度比内存高出几个数量级,为了 CPU 能够更高效地与内存进行交互,在 CPU 和内存之间设计了多层缓存机制,如下图所示。
一般来说,CPU 会分为三级缓存,分别为L1 一级缓存、L2 二级缓存和L3 三级缓存。越靠近 CPU 的缓存,速度越快,但是缓存的容量也越小。所以从性能上来说,L1 > L2 > L3,容量方面 L1 < L2 < L3。
CPU 读取数据时,首先会从 L1 查找,如果未命中则继续查找 L2,如果还未能命中则继续查找 L3,最后还没命中的话只能从内存中查找,读取完成后再将数据逐级放入缓存中。此外,多线程之间共享一份数据的时候,需要其中一个线程将数据写回主存,其他线程访问主存数据。
由此可见,引入多级缓存是为了能够让 CPU 利用率最大化。如果你在做频繁的 CPU 运算时,需要尽可能将数据保持在缓存中。
# 什么是缓存行
Cache Line 是 CPU 缓存可操作的最小单位,CPU 缓存由若干个 Cache Line 组成。Cache Line 的大小与 CPU 架构有关,在目前主流的 64 位架构下,Cache Line 的大小通常为 64 Byte。Java 中一个 long 类型是 8 Byte,所以一个 Cache Line 可以存储 8 个 long 类型变量。CPU 在加载内存数据时,会将相邻的数据一同读取到 Cache Line 中,因为相邻的数据未来被访问的可能性最大,导致缓存好行失效,这样导致 CPU 频繁与内存进行交互。
查看缓存行命令:
Mac:sysctl machdep.cpu.cache.linesize
Linux:getconf -a|grep CACHE 或者 cat /proc/cpuinfo |grep -I cache
2
3
4
# 伪共享如何产生的
- 假设变量 x,y 被加载到同一个 Cache Line,它们会被高频地修改。
- 当线程 1 在 CPU Core1 中对变量 x 进行修改,修改完成后 CPU Core1 会通知其他 CPU Core 该缓存行已经失效。
- 然后线程 2 在 CPU Core2 中对变量 y 进行修改时,发现 Cache line 已经失效,此时 CPU Core1 会将数据重新写回内存,CPU Core2 再从内存中读取数据加载到当前 Cache line 中。
# 缓存一致性协议MESI (opens new window)
每个处理器都有自己的高速缓存,而又共享同一主内存。当多个处理器都涉及同一块主内存区域的更改时,将导致各自的的缓存数据不一致。那同步到主内存时该以谁的缓存数据为准呢?
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,来保证处理器间缓存的一致性。这类协议有MSI、MESI、MOSI等。
MESI是Modified(修改)、Exclusive(独占)、Shared(共享)、Invaild(失效)四种状态的缩写,是用来修饰缓存行的状态。在每个缓存行前额外使用2bit,来表示此四种状态。
- Modified(修改):该缓存行仅出现在此cpu缓存中,缓存已被修改,和内存中不一致,等待同步至内存。
- Exclusive(独占):该缓存行仅出现在此cpu缓存中,缓存和内存中保持一致。
- Shared(共享):该缓存行可能出现在多个cpu缓存中,且多个cpu缓存的缓存行和内存中的数据一致。
- Invalid(失效):由于其他cpu修改了缓存行,导致本cpu中的缓存行失效。
# 如何规避伪共享
目前为止最有效的办法就是,缓存行填充。空间换时间。
Java8优雅解决:@sun.misc.Contended 注意:JVM启动参数加上-XX:-RestrictContended才会生效 最佳实践:ConcurrentHashMap.CounterCell