记录缓存伪共享问题以及解决办法

伪共享指的是多个线程同时读写同一个缓存行的不同变量时导致的 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个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。

因此如果你数据结构中的项在内存中不是彼此相邻的(链表),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。

转载于:https://my.oschina.net/6582886lp/blog/2999347

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值