1.CPU缓存

要了解什么是伪共享,首先得了解CPU缓存架构与缓存行的知识

(1)<CPU缓存架构>

主内存RAM是数据存在的地方,CPU和主内存之间有好几级缓存,因为即使直接访问主内存相对来说也是非常慢的。如果对一块数据做相同的运算多次,那么在执行运算的时候把它加载到离CPU很近的地方就有意义了,避免每次都到主内存中去取这个数据。
FalseSharing-伪共享-小白菜博客
越靠近CPU的缓存越快也越小,所以L1缓存很小但很快,并且紧靠着在使用它的CPU内核。
L2大一些,但也慢一些,并且仍然只能被一个单独的CPU核使用。
L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有CPU核共享。
最后,主内存保存着程序运行的所有数据,它更大,更慢,由全部插槽上的所有CPU核共享。
FalseSharing-伪共享-小白菜博客
当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。
走得越远,运算耗费的时间就越长。所以如果进行一些很频繁的运算,要确保数据在L1缓存中。

(2)<CPU缓存行>

缓存失效其实指缓存行失效,Cache是由很多个Cache line 组成的,每个缓存行大小是32~128字节(通常是64字节)。我们这里假设缓存行是64字节,而java的一个Long类型是8字节,这样的话一个缓存行就可以存8个Long类型的变量,如下图所示。CPU 每次从主内存中获取数据的时候都会将相邻的数据存入到同一个缓存行中。假设我们访问一个Long内存对应的数组的时候,如果其中一个被加载到内存中,那么对应的后面的7个数据也会被加载到对应的缓存行中,这样就会非常快的访问数据。
image.png

2.伪共享

根据MESI协议(缓存一致性协议),我们知道在一个缓存中的数据变化的时候会将其他所有存储该缓存的缓存(其实是缓存行)都失效。

(1)<示例概述>

下图中显示的是一个槽的情况:里面是多个CPU, 如果CPU1上面的线程更新了变量X,根据MESI协议,那么变量X对应的所有缓存行都会失效(由于X和Y被放到了一个缓存行,所以一起失效了),这个时候如果cpu2中的线程进行读取变量Y,发现缓存行失效,想获取Y就会按照缓存查找策略,往上查找。如果期间cpu1对应的线程更新X后没有访问X(也就是没有刷新缓存行),cpu2的线程就只能从主内存中获取数据,对性能就会造成很大的影响,这就是伪共享。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。
image.png

(2)<解决方法>

这个问题的解决办法有两个:
1.使用对齐填充,因为一个缓存行大小是64个字节,如果读取的目标数据小于64个字节,可以增加一些无意义的成员变量来填充。
2.在Java8里面,提供了@Contented注解,它也是通过缓存行填充来解决伪共享问题的,被@Contented注解声明的类或者字段,会被加载到独立的缓存行上。