showdialog 尝试读取或写入受保护的内存_JVM系列之五[JVM内存模型-前奏之硬件层面]...

为了最大化的利用 CPU 提升性能,从硬件、操作系统、编译器等方面都做出了很多的优化

  1. CPU加高速缓存
  2. 操作系统加进程、线程:通过CPU时间片切换最大化提升CPU的使用率
  3. 编译器指令优化:合理的利用CPU的高速缓存

为了解决计算机中的主内存与CPU之间的运行速度差问题,在CPU与主内存之间添加一级或多级高速缓冲存储器(Cache),将运算所需的数据复制到缓存中,让运算更快运行,运算结束后从缓存中同步会内存中,这样处理器无须等待内存的读写,而这个Cache是被集成到CPU内部,称为CPU Cache。

7062b18d3b1f0e1f3109a8488a9c71c5.png

CPU高速缓存

扩展:CPU缓存行

Cache内部按行存储,每一行称为一个Cache行。Cache行是Cache与主内存进行数据交换的单位,Cache行的大小一般是2的幂次数字节。

24cfe88681165a1c109eedebbc3851fa.png

CPU缓存行

当CPU访问某变量时,会先看CPU Cache内是否有改变量,如有则从中获取,否则从主内存中获取该变量,然后把该变量所在主内存区域的一个Cache行大小的内存复制到Cache行,缓存与主内存交换数据的单位就是缓存行。

Cache行中存储的是内存块而不是单个变量,因此可能在一个Cache行中存储多个变量。

伪共享
当多个线程同时修改一个缓存行中的多个变量时,由于同时只能有一个线程操作缓存行,相比将每个变量放到一个缓存行,性能会有所下降,称为伪共享。

8ee8968826aa2a4198aac99e666abb61.png

变量在内存中的存储位置

变量x与y属于同一个缓存行,在被使用时被放到了CPU的一级、二级缓存。Thread1修改修改x变量时,先修改一级缓存中x的缓存行,在缓存一致性协议下,CPU2中的x变量对应的缓存行失效。Thread2在写入变量x时只能从二级缓存查找,此时破坏了一级缓存,但是一级缓存比二级缓存更快,说明多个线程不可能同时修改当下CPU中相同缓存行里的变量。

伪共享产生的原因

  • 多个变量放入同一个缓存行,多个线程同时写入缓存行中的不同变量。

如何避免伪共享:

  • jdk8之前通过字节填充来避免,即在创建变量时使用填充字段填充变量所在缓存行;如果缓存行64个字节,类FilledLong中填充6个long类型变量,(64=8*7+8(对象头))如下:
public final class FilledLong {    public volatile long value = 0L;    public long p1,p2,p3,p4,p5,p6;}
  • jdk8使用sun.misc.Contended注解(可以修饰类或变量)解决伪共享,@Contended注解默认情况只能用在Java核心类(如rt.jar),如用户类路径下使用时JVM需加参数-XX:RestrictContended,默认填充宽度128字节,自定义宽度时需设置-XX:ContendedPaddingWidth=128

缓存一致性问题:

在多处理器系统中,每个处理器都有自己的高速缓存,且共享同一主内存(Main Memory),当多处理器的运算任务都涉及同一块主内存区域时,不同CPU中运行的不同线程看到的同一份内存中的缓存值会不一样,将可能导致各自的缓存数据不一致。

CPU层面有两种方式来解决缓存不一致问题:

  1. 总线锁:多CPU情况下,对共享内存操作,总线上发送LOCK#信号,其他CPU无法通过总线访问到共享内存中数据,总线锁将CPU和共享内存的通信锁定,其他CPU无法操作其他内存地址数据,这样的开销会很大,影响机器性能。
  2. 缓存锁:为了达到数据访问时的一致性,需要CPU在访问缓存行时遵循一些协议,在读写缓存时根据协议操作,常见协议如MSI、MESI、MOSI等。

MESI协议:

  • M(Modify),共享数据只存在当前CPU缓存中,且是被修改状态,也就是缓存中数据和主内存中数据不一致
  • E(Exclusive),缓存的独占状态,数据只缓存在当前CPU缓存中,没有被修改
  • S(Shared),数据可能被多个CPU缓存,各个缓存中的数据与主内存数据一致
  • I(Invalid),缓存无效

MESI协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也能监听其他Cache的读写操作。

22063ec55a61ffb19a8927feca8fcc29.png
b0ad340ef707d554ec473ac5763139c2.png
7c51088fd0ef02f2872868eab9f48526.png

对于MESI 协议,从CPU读写角度来说需遵循以下原则:

  • CPU读请求:缓存处于M、E、S状态都可以被读取,I状态CPU只能从主存中读取数据
  • CPU写请求:缓存处于M、E状态才可以被写。对于S状态的写,需要将其他CPU中缓存行置为无效才可写

高速缓存的出现,使得每个CPU中都缓存了相同的共享数据,可能会出现可见性问题,因为CPU1修改了本地的缓存的值对CPU2不可见,而CPU2再对相同数据进行写操作时,使用的是脏数据,使得结果不可测。


MESI 协议可以实现缓存的一致性,但会带来资源浪费问题:

92ab50256ab7e35549deba9492288b6a.png

CPU缓存行的状态是通过消息来进行传递的。 如CPU0 要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的CPU,且要等到他们的确认回执。CPU0 在这段时间内都会处于阻塞状态。为了避免阻塞带来的资源浪费,CPU引入了Store Buffers

76229b6a8994c20c382bac0937a0b655.png

CPU0在写入共享数据时,将数据写入到StoreBuffer中,同时发送Invalidate消息给其他CPU,继续处理其他指令(异步操作)。当收到其他所有CPU发送了Invalidate Acknowledge消息时,再将StoreBuffer中的数据存储至缓存行中,最后再从缓存行同步到主内存。这种异步的操作可能会引起CPU 的重排序,进而带来可见性问题。

StoreBuffer存在两个问题:

  1. 数据的提交时间不确定,需要等待其他CPU回复后才能进行数据同步,这是异步操作
  2. 引入了Store Buffere后,处理器读取数据顺序为:先尝试从StoreBuffer中读,如果有数据,则直接读取,否则就再从缓存行中读取

CPU为解决可见性问题,提供了内存屏障指令,也就是CPU flush store bufferes中的指令。

内存屏障(Memory Barrier):将Store Buffers中的指令写入内存,从而使得访问同一共享内存的线程都可见。

X86的内存屏障:

  1. 读屏障(Ifence):读屏障之后的读操作,都在读屏障后执行
  2. 写屏障(sfence,store memory barrier):写屏障之前的指令结果对之后的指令都可见
  3. 全屏障(mfence):全屏障前的内存读写操作结果提交到内存后,在执行屏障后的读写操作

内存屏障作用:通过防止CPU对内存的乱序访问,来保证共享数据在多线程并行执行下的可见性

综上所述,内存可见性的根本原因是缓存和重排序,而JMM提供了合理禁用缓存及重排序的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值