伪共享指的是多个线程同时读写同一个缓存行的不同变量时导致的 CPU 缓存失效
首先,要了解缓存伪共享必须知道的是CPU的三级缓存与内存之前的数据交换.
一、cpu缓存结构
CPU缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。和计算机存储金字塔结构类似,高速缓存也可以按照金字塔结构,从下到上越接近CPU速度越快,同时容量也越小。从下到上依次为 三级, 二级,一级缓存. 一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取.
二、缓存行 CacheLine
缓存行时缓存和内存交换数据的最小单位,一般为64字节。即每次缓存和内存之间进行数据交换的时候,都是以字节对齐的连续64字节的内存块整个进行。
三、多核CPU缓存
多核CPU,每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。多CPU,每个CPU都相互独立,拥有自己的缓存,CPU之间无法共享缓存.此时会出现高速缓存与变量之间读写问题.例如:
1.CPU0读取一个字节,并把它写入CPU0的高速缓存行
2.CPU1读取一个字节,并把它写入CPU1的高速缓存行
3.CPU0修改这个字节,CPU1被告知这个情况,CPU1的高速缓存行无效
4.CPU1重新读取内存的值,并写入高速缓存行
假如多个变量(例如数组)由于在主内存中邻近,存在于同一个缓存行之中,它们的相互改变会导致频繁的缓存未命中,引发性能下降。
四、解决办法
字节填充 : 需要保证不同线程的变量存在于不同的 CacheLine 即可,使用多余的字节来填充可以做点这一点,这样就不会出现伪共享问题
实现字节填充
// long padding避免false sharing
// 按理说jdk7以后long padding应该被优化掉了,但是从测试结果看padding仍然起作用
public final static class VolatileLong {
volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0L;
}
VolatileLong 类中需要保存一个 long 类型的 value 值,如果多线程操作同一个 CacheLine 中的 VolatileLong 对象,便无法完全发挥出 CPU Cache 的优势。
不知道你注意到没有,实际数据 value + 用于填充的 p0~p6 总共只占据了 7 * 8 = 56 个字节,而 Cache Line 的大小应当是 64 字节,这是有意而为之,在 Java 中,对象头还占据了 8 个字节,所以一个 VolatileLong 对象可以恰好占据一个 Cache Line。
在声明变量时候使用volatile,volatile变量要求在更新了缓存之后立即写入到系统内存,而非volatile变量,则是CPU修改缓存,缓存在适当的时候(不知道什么时候)将缓存数据写入内存
Java8 中实现字节填充
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
String value() default "";
}
/**
* jdk8新特性,Contended注解避免false sharing
* Restricted on user classpath
* Unlock: -XX:-RestrictContended
*/
@SuppressWarnings("restriction")
@sun.misc.Contended
public final static class VolatileLong3 {
public volatile long value = 0L;
}
Java8 中终于提供了字节填充的官方实现,这无疑使得 CPU Cache 更加可控了,无需担心 jdk 的无效字段优化,无需担心 Cache Line 在不同 CPU 下的大小究竟是不是 64 字节。使用 @Contended 注解可以完美的避免伪共享问题。注意在运行时需要设置JVM启动参数-XX:-RestrictContended
具体例子:https://www.jianshu.com/p/c3c108c3dcfd
五、总结
伪共享是实实在在的问题,而且相当隐蔽。但现在Java的多线程库,都会考虑到这个问题。例如Disruptor还有Netty都会使用Padding的类代替原有类,达到去除伪共享的目的。了解这些之后对于多线程的缓存问题也就一并都懂了
数组与链表数据结构也就大致了解:
如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。
因此如果你数据结构中的项在内存中不是彼此相邻的(链表),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。