• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

Java内存模型(一)

互联网 diligentman 2周前 (04-29) 10次浏览

文章目录

  • 1 缓存行
  • 2 CPU的数据一致性
  • 3 伪共享
    • 3.1 什么是伪共享
    • 3.2 如何预防伪共享问题
  • 4 指令重排序
  • 5 防止指令重排序
    • 5.1 硬件层面防止指令重排序
      • 5.1.1 硬件内存屏障
      • 5.1.2 原子指令
    • 5.2 JVM规范(JSR133)

1 缓存行

  说到缓存行,首先得了解CPU多核三级缓存架构。
Java内存模型(一)
  首先有一个常识,CPU的速度是主存的100倍。为了协调CPU和主存之间的速度差异,在CPU和主存之间,加入了三级缓存,其速度L1 > L2 > L3。CPU对主存中的某个数据进行读写操作时,先将数据从主存加载到L3,再到L2,最后到L1。根据空间局部性原理,CPU为了提高效率,减少从主存中加载数据的频率,并非每次从主存中加载一个数据,而是按块加载,这个数据块在缓存中成为缓存行(CacheLine),每个缓存行占64个字节。

2 CPU的数据一致性

  CPU的多核三级缓存架构,导致了多个核之间的L1、L2数据不一致的问题。试想L3中的一个缓存行存放了一个数据num,这个缓存行被加载到CPU中core1和core2的L1中。core1对num进行更新操作,此时core2并不知道core1中数据的变化,两个core中缓存的数据出现了不一致。

2.1 缓存一致性协议

  为了解决CPU中数据不一致的问题,诞生了缓存一致性协议。缓存一致性协议有很多种实现,比如:MSI、MESI、MOSI、Synapse、Firefly和DragonProtocol等。其中,最著名的缓存一致性协议当属Intel的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。

2.2 锁总线

  某些情况下,缓存一致性协议也具有局限性。比如某些数据无法被缓存,以及无法保证多个CPU之间的缓存一致性。这时候,就需要在总线上加锁来实现了。

3 伪共享

3.1 什么是伪共享

  当多线程修改同一个缓存行中互相独立的变量时,因为CPU需要遵守缓存一致性协议,会无意中影响线程执行的效率,这就是伪共享。设想在同一个缓存行中存放了两个变量a和b,这个缓存行被加载到core1和core2的缓存中。core1要对a进行修改,core2要对b进行修改。当core1修改了a必然会影响到core2,同理当core2修改了b必然会影响到core1,两个核之间需要频繁地经过L3来同步数据,所以core1和core2的性能会大打折扣。并且更为致命的是,从代码中并不能知道是否存在伪共享的问题。

3.2 如何预防伪共享问题

  为了预防伪共享产生的性能问题,一般可以借助缓存行对齐来实现,比如Disruptor就使用了大量的缓存行对齐编码手段,这是一个将性能提现到极致的单机消息队列中间件,将会在后续的文章中进行具体介绍。

4 指令重排序

4.1 CPU的指令重排序

  现代处理器为了提升其指令执行的效率,采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

4.2 编译器指令重排序

  编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。

5 防止指令重排序

  某些情况下,我们并不希望指令重排序发生,参考《多线程之volatile》。在硬件层面,由硬件内存屏障和原子指令防止指令重排序。而在JVM层面,是由JVM内存屏障来实现的,而JVM的内存屏障最终还是需要依赖于硬件的实现。

5.1 硬件层面防止指令重排序

5.1.1 硬件内存屏障

  在硬件层面,主要是利用CPU的内存屏障来禁止指令重排序。x86架构的CPU,有三种内存屏障:

  • sfence(Store Fence):在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
  • lfence(Load Fence):在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
  • mfence(Memory Fence):在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

5.1.2 原子指令

  除了内存屏障以外,还可以通过原子指令来实现禁止指令重排序。如x86上的lock指令。这个lock指令其实是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。

5.2 JVM规范(JSR133)

  JVM规范中定义了四种内存屏障:

  • LoadLoad:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

  JVM只是对内存屏障定义了一个规范,而不同的虚拟机可能会有不同的实现,但其最终肯定是依赖于硬件来实现的。

5.2.1 volatile

  JVM对于volatile内存区的读写操作,都会加屏障:

  • 在每个写操作之前加StoreStoreBarrier。
  • 在每个写操作之后加StoreLoadBarrier。
  • 在每个读操作之前加LoadLoadBarrier。
  • 在每个读操作之后加LoadStoreBarrier。

5.2.2 synchronized

  synchronized代码块编译成字节码后,会在代码块前后加上monitorenter指令和monitorexit指令。其在JVM中调用了系统提供的同步机制(C或C++实现),而在硬件层面,是通过lock comxchg指令实现(x86架构)。


程序员灯塔
转载请注明原文链接:Java内存模型(一)
喜欢 (0)