缓存一致性协议和volatile

    在阅读LongAdder源码的过程中,碰到了@sun.misc.Contended的使用,在百度了一下后了解到该注解是为了解决伪共享问题。

   什么是伪共享?

    要想彻底了解伪共享,首先要对缓存行(也可称为缓存段),和缓存一致性协议有所了解。个人在网上找了许多资料认为:缓存一致性(Cache Coherency)入门,这篇简绍的比较好,建议大家阅读一下。

    以及了解缓存一致性协议(MESI)的具体流程这里我推荐:缓存一致性协议的工作方式(但是里面在脏段被修改后,其它CPU依然可以获取到脏段进行修改,只是在最后被替换的时候进行回写,类似于缓存一致性(Cache Coherency)入门提到的MOSEI 协议)

    现在可以解释伪共享了:当a,b两个变量同时在一个缓存段里,CPU1对a变量进行修改会使所有内核的缓存同时失效,此时CPU2在读取b变量的缓存时,CPU1缓存段触发回写,CPU2则在回写后从内存中读取最新变量去获取最新缓存段在放到自己的缓存中。

    这样对a变量的修改会导致b变量缓存的失效,而在CPU申请缓存行独占时也会可能发生竞争,由于变量a和遍历b“共享”了同一个缓存段,大大影响了性能,这就是伪共享。

   缓存一致性协议和volatile

     我想到了这样一个问题,volatile的其中一个功能是保证变量的可见性,那么既然已经有了缓存一致性协议去保证CPU读到的遍历是同一份,那么为什么还需要volatile的可见性呢?

    1、弱内存模型的体系结构不能完全保证缓存完整的顺序一致性

      在缓存一致性(Cache Coherency)入门文章中提到缓存一致性协议至少在原理上,提供了完整的顺序一致性。

      规则是这样的:如果满足下面的条件,你就可以得到完全的顺序一致性:第一,缓存一收到总线事件,就可以在当前指令周期中迅速做出响应。第二,处理器如实地按程序的顺序,把内存操作指令送到缓存,并且等前一条执行完后才能发送下一条。当然,

      实际上现代处理器一般都无法满足以上条件:

  • 缓存不会及时响应总线事件。如果总线上发来一条消息,要使某个缓存段失效,但是如果此时缓存正在处理其他事情(比如和 CPU 传输数据),那这个消息可能无法在当前的指令周期中得到处理,而会进入所谓的“失效队列(invalidation queue)”,这个消息等在队列中直到缓存有空为止。
  • 处理器一般不会严格按照程序的顺序向缓存发送内存操作指令。当然,有乱序执行(Out-of-Order execution)功能的处理器肯定是这样的。顺序执行(in-order execution)的处理器有时候也无法完全保证内存操作的顺序(比如想要的内存不在缓存中时,CPU 就不能为了载入缓存而停止工作)。
  • 写操作尤其特殊,因为它分为两阶段操作:在写之前我们先要得到缓存段的独占权。如果我们当前没有独占权,我们先要和其他处理器协商,这也需要一些时间。同理,在这种场景下让处理器闲着无所事事是一种资源浪费。实际上,写操作首先发起获得独占权的请求,然后就进入所谓的由“写缓冲(store buffer)”组成的队列(有些地方使用“写缓冲”指代整个队列,我这里使用它指代队列的一条入口)。写操作在队列中等待,直到缓存准备好处理它,此时写缓冲就被“清空(drained)”了,缓冲区被回收用于处理新的写操作。

     这些特性意味着,默认情况下,读操作有可能会读到过时的数据(如果对应失效请求还等在队列中没执行),写操作真正完成的时间有可能比它们在代码中的位置晚,一旦牵涉到乱序执行,一切都变得模棱两可。

     在弱内存模型的体系结构中,处理器为了开发者能写出正确的代码而做的工作是最小化的,指令重排序和各种缓冲的步骤都是被正式允许的,也就是说没有任何保证。如果你需要确保某种结果,你需要自己插入合适的内存屏障——它能防止重排序,并且等待           队列中的操作全部完成。

    2、volatile会使用内存屏障,来保证缓存一致性协议完美的顺序一致性。

      到这里我又在网上找了一些资料,volatile与lock前缀指令中提到,对voilatile反编译后的汇编指令进行分析发现对volatile修饰的变量写操作指令多了一个lock前缀,那么内存屏障呢?好像在汇编指令里并没有看到,所以我又进行搜索,有的说lock指令并不是内存屏障但是可以完成和内存屏障类似的功能,又有的文章说volatile是使用内存屏障。最后我找到一篇解释比较合理的文章:volatile的原理到底是lock前缀指令还是内存屏障?
      里面解释道:在x86架构下,LoadLoad、LoadStore、StoreStore三种屏障是空操作,StoreLoad通过lock前缀指令实现。

所以lock前缀指令是内存屏障的一种实现。

最近在拜读:JVM内存模型、指令重排、内存屏障概念解析 - 陈洋Cy - 博客园

有了新的感悟:内存屏障是一种CPU指令,分为四种类型LoadLoad,StoreStore, LoadStore,StroeLoad, volatile原则上来说会为读写请求插入这四种内存屏障,但是在Intel 64/IA-32架构的处理器上,LoadLoad,StoreStore, LoadStore为空操作,storeLoad通过lock前缀指令实现。

   如何利用缓存一致性协议解决伪共享问题?

Java8之前可以给变量前后分别填充8个long类型的字段解决。(为什么总共是8个?因为一个不同CPU架构的缓存行是不一样的,最常见的缓存行大小是64字节,而8个long类型刚好是64字节)

Java 8 中已经提供了官方的解决方案,Java 8 中新增了一个注解: @sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在 jvm 启动时设置 -XX:-RestrictContended 才会生效。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值