cpu缓存系统中是以缓存行(cache line)为单位存储的。目前主流的cpu cache的cache line大小都是64bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(false sharing)。
由于cpu的速度远远大于内存速度,所以cpu设计者们就给cpu加上了缓存(cpu cache)。 以免运算被内存速度拖累。(就像我们写代码把共享数据做cache不想被db存取速度拖累一样),cpu cache分成了三个级别:l1,l2,l3。级别越小越接近cpu, 所以速度也更快, 同时也代表着容量越小。
cpu获取数据回依次从l1,l2,l3中查找,如果都找不到则会直接向内存查找。
由于共享变量在cpu缓存中的存储是以缓存行为单位,一个缓存行可以存储多个变量(存满当前缓存行的字节数);而cpu对缓存的修改又是以缓存行为最小单位的,那么就会出现上诉的伪共享问题。
cache line可以简单的理解为cpu cache中的最小缓存单位,今天的cpu不再是按字节访问内存,而是以64字节为单位的块(chunk)拿取,称为一个缓存行(cache line)。当你读一个特定的内存地址,整个缓存行将从主存换入缓存,并且访问同一个缓存行内的其它值的开销是很小的。
看如下代码示例:
表面上看,第二个循环工作量为第一个循环的1/16;但是执行时间是相差不大的,假设在内存规整的情况下,每16个int 占用4*16=64字节,正好一个缓存行,也就是说这两个循环访问内存的次数是一致的。导致耗时相差不大。
目前常用的缓存设计是n路组关联(n-way set associative cache),他的原理是把一个缓存按照n个cache line作为一组(set),缓存按组划为等分。每个内存块能够被映射到相对应的set中的任意一个缓存行中。比如一个16路缓存,16个cache line作为一个set,每个内存块能够被映射到相对应的set
中的16个cacheline中的任意一个。一般地,具有一定相同低bit位地址的内存块将共享同一个set。
下图为一个2-way的cache。由图中可以看到main memory中的index0,2,4都映射在way0的不同cacheline中,index1,3,5都映射在way1的不同cacheline中。

多核cpu都有自己的专有缓存(一般为l1,l2),以及同一个cpu插槽之间的核共享的缓存(一般为l3)。不同核心的cpu缓存中难免会加载同样的数据,那么如何保证数据的一致性呢,就是mesi协议了。
在mesi协议中,每个cache line有4个状态,可用2个bit表示,它们分别是:
m(modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本cache中;
e(exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本cache中;
s(shared):这行数据有效,数据和内存中的数据一致,数据存在于很多cache中;
i(invalid):这行数据无效。
那么,假设有一个变量i=3(应该是包括变量i的缓存块,块大小为缓存行大小);已经加载到多核(a,b,c)的缓存中,此时该缓存行的状态为s;此时其中的一个核a改变了变量i的值,那么在核a中的当前缓存行的状态将变为m,b,c核中的当前缓存行状态将变为i。如下图:
那么为什么会出现伪共享问题呢?上诉的情况再扩展一下,假设在多线程情况下,x,y两个共享变量在同一个缓存行中,核a修改变量x,会导致核b,核c中的x变量和y变量同时失效。
此时对于在核a上运行的线程,仅仅只是修改了了变量x,却导致同一个缓存行中的所有变量都无效,需要重新刷缓存(并不一定代表每次都要从内存中重新载入,也有可能是从其他cache中导入数据,具体的实现要看各个芯片厂商的实现了)。
假设此时在核b上运行的线程,正好想要修改变量y,那么就会出现相互竞争,相互失效的情况,这就是伪共享啦。
执行结果:
现在,我们将volatilelong中不使用的6个long变量注释掉,再次执行:
可以看到,两个程序逻辑完全一致,只是注释掉了几个没有使用到的变量,却导致性能相差很大。 我们知道一条缓存行有64字节, 而java程序的对象头固定占8字节(32位系统)或12字节(64位系统默认开启压缩, 不开压缩为16字节). 我们只需要填6个无用的长整型补上6*8=48字节, 让不同的volatilelong对象处于不同的缓存行, 就可以避免伪共享了(64位系统超过缓存行的64字节也无所谓,只要保证不同线程不要操作同一缓存行就可以)。这个办法叫做补齐(padding)。
java8中已经提供了官方的解决方案,java8中新增了一个注解:@sun.misc.contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-xx:-restrictcontended才会生效。
运行结果: