伪共享问题-并发编程无声的性能杀手
本文以LongAdder源码为例进行说明。关于原子累加器的论述可以参考文章:原子累加器LongAdder与AtomicLong
1.LongAdder部分源码
2.多核机器的存储结构
CPU为了提升性能,在设计上都设计了多级缓存。一个CPU会分多个核心,每个CPU核心都有自己的一级缓存、二级缓存,多个核心之间可以共享三级缓存,多个CPU之间可以共享内存。
时间对比
3.伪共享问题
因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率。而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)。缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。CPU要保证数据的一致性,如果某个CPU核心更改了数据,其它CPU核心对应的长高缓存行必须失效。
因为Cell是数组形式,在内存中是连续存储的,一个Cell为24字节(16个字节的对象头和8字节的value),因此缓存行可以存下2个的Cell对象,这样问题就来了:
Core-0 要修改Cell[0]
Core-1 要修改Cell[1]
无论谁修改成功,都会导致对方Core的缓存行失效,比如Core-0中Cell[0]=6000,Cell[1]=8000要累加Cell[0]=6001,Cell[1]=8000,这时会让Core-1的缓存行失效。
总结:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。即由于读写对应同一块内存的缓存行失效,导致的效率降低。
4.伪共享问题的解决方案
@sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128自己大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。
在对象前后加上间隙,防止占用同一个缓存行,从而避免伪共享。代价就是让对象占用的内存变大了,鱼与熊掌不能兼得。