简述一下伪共享的概念以及如何避免
缓存行
计算机工程师为了提高 CPU 的利用率,平衡 CPU 和内存之间的速度差异,在CPU 里面设计了三级缓存。
CPU 在向内存发起 IO 操作的时候,一次性会读取 64 个字节的数据作为一个缓存行,缓存到 CPU 的高速缓存里面。
在 Java 中一个 long 类型是 8 个字节,意味着一个缓存行可以存储 8 个 long 类型的变量。
这个设计是基于空间局部性原理来实现的,也就是说,如果一个存储器的位置被引用,
那么将来它附近的位置也会被引用。
所以缓存行的设计对于 CPU 来说,可以有效的减少和内存的交互次数,从而避免了 CPU
的 IO 等待,以提升 CPU 的利用率。
伪共享问题
正是因为这种缓存行的设计,导致如果多个线程修改同一个缓存行里面的多个独立变量的时候,基于缓存一致性协议,就会无意中影响了彼此的性能,这就是伪共享的问题。 (如图)像这样一种情况,CPU0 上运行的线程想要更新变量 X、CPU1 上的线程想要更新变量 Y,而 X/Y/Z 都在同一个缓存行里面。每个线程都需要去竞争缓存行的所有权对变量做更新,基于缓存一致性协议。一旦运行在某个 CPU 上的线程获得了所有权并执行了修改,就会导致其他 CPU 中的缓存行失效。
这就是伪共享问题的原理。
解决问题
因为伪共享会问题导致缓存锁的竞争,所以在并发场景中的程序执行效率一定会收到较大的影响。这个问题的解决办法有两个:
- 使用对齐填充,因为一个缓存行大小是 64 个字节,如果读取的目标数据小于 64个字节,可以增加一些无意义的成员变量来填充。
- 在 Java8 里面,提供了@Contented 注解,它也是通过缓存行填充来解决伪共享问题的,被@Contented 注解声明的类或者字段,会被加载到独立的缓存行上。