JVM-JMM硬件层数据一致性 Volicate和synchronized实现细节

1.存储器的层级次结构

在这里插入图片描述1.L3在主板所有cpu共享,L1 L2 寄存器在计算机cpu内部, L3L4 L5 L6的数据被加载到不同的cpu 不同CPU赋值不一样,造成不一致性。
解决:总线锁,同一时刻只有一个线程CPU可以访问,一个cpu使用变量不会被其他的改变。
从下往上,成本越来越高,读取速度也越来越贵。
对于现代cpu而言,性能瓶颈则是对于内存的访问。cpu的速度往往都比主存的高至少两个数量级。因此cpu都引入了L1_cache与L2_cache,更加高端的cpu还加入了L3_cache.

2.cache line的概念 缓存行对齐 伪共享

在这里插入图片描述
读取缓存以cache line为基本单位,目前64byte位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题,解决方法:变量前后各存储64bytes的数据,只是用来占据cacheline,保证想要操作的数据只被一个cpu锁定,避免互相影响。
实际使用的开源框架:
disruptor 前后各有7个long 填充缓存行凑够64 避免伪共享
在这里插入图片描述
现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁

3.总线锁

在这里插入图片描述
同一缓存行的两个不同数据被两个CPU锁定,为了防止一个CPU使用的时候另一个CPU也使用,所以用总线锁,保持独占。

4.MESI Cache一致性协议.

支持写回策略的缓存一致性协议。
在这里插入图片描述
MESI协议中的状态
CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):

M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。

当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。

同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

I: 无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。
在对一个数据进行计算的时候发现是无效的,会重新加载入内存再读一遍就会变成有效的;
现代CPU的数据一致性的实现=缓存锁(MESI等各种协议)+总线锁

5. 现代CPU合并写和CPU乱序执行指令的原因

对于现代cpu而言,性能瓶颈则是对于内存的访问。cpu的速度往往都比主存的高至少两个数量级。因此cpu都引入了L1_cache与L2_cache,更加高端的cpu还加入了L3_cache。
如果CPU访问的数据在缓存没找到,会去主存去找,在去找的这段时间会继续执行不依赖的指令,这就是乱序执行的原因。
当cpu执行存储指令时,它会首先试图将数据写到离cpu最近的L1_cache, 如果此时cpu出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,cpu就会使用一个另外的缓冲区,叫做合并写存储缓冲区。这一技术称为合并写入技术。在请求L2_cache缓存行的所有权尚未完成时,cpu会把待写入的数据写入到合并写存储缓冲区,该缓冲区大小和一个cache line大小,一般都是64字节。这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了cpu写数据时cache miss时的性能影响。

当后续的写操作需要修改相同的缓存行时,这些缓冲区变得非常有趣。在将后续的写操作提交到L2缓存之前,可以进行缓冲区写合并。 这些64字节的缓冲区维护了一个64位的字段,每更新一个字节就会设置对应的位,来表示将缓冲区交换到外部缓存时哪些数据是有效的。当然,如果程序读取已被写入到该缓冲区的某些数据,那么在读取缓存数据之前会先去读取本缓冲区的。

经过上述步骤后,缓冲区的数据还是会在某个延时的时刻更新到外部的缓存(L2_cache).如果我们能在缓冲区传输到缓存之前将其尽可能填满,这样的效果就会提高各级传输总线的效率,以提高程序性能。
在实际使用缓冲区的时候,实际上在使用时一次写入正好占满缓冲区这样效率最高,如果一次写入太多,缓冲区放不下,CPU就要暂停等待CPU缓冲区执行完再继续放入,比较浪费时间。所以一次写入刚好放满缓冲区最好。

6.保障有序性 Volicate和synchronized

1.有序性保障
X86 CPU内存屏障
sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
2.有序性保障
intel lock汇编指令
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
3.JSR内存屏障
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

Volicate
变量前加上Volicate可以保证有序性,防止指令重排。
volatile的实现细节
一:编译器层面
编译过的class文件,变量前加上访问符ACC_VOLICATE。
在这里插入图片描述
二:JVM层面
当jvm看到ACC_VOLATILE到底怎么实现呢这要跟你jvm的实现有关系。jvm的实现层级实际上是干了这么一件事,它对所有内存volatile写操作前面加了StoreStoreBarrier后面加了一个StoreLoadBarrier,在所有volatile的读操作前加了LoadLoadBarrier后面加了LoadStoreBarrier。想一想如果前面加了LoadLoad上面的L1和小面的读操作一定不能是重排序的,下面加了一个LoadStore上面这个读操作和下面任何的这种修改的操作坑定是不能重排序的。所以就保证了在读的过程中不会有任何不一致的情况,同样的写操作也是一样。这个volatile的实现细节。 volatile内存区的读写 都加屏障。
jvm对所有内存volicate写操作前加上了如下两个指令

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier

jvm对所有内存volicate读操作前加上了如下两个指令

LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

三:操作系统及硬件层面
所以volatile所谓的实现细节首先要分不同的层次,它首先写源码时候把它编译完它只是在字节码上也就是class文件上给你加了ACC_VOLATILE这么一个标志。当虚拟机读到这个标志的时候它就会在内存区读写之前都加屏障。加完之后虚拟机和操作系统去执行这个虚拟机程序然后读到这个东西的时候在硬件层面是怎么实现的呢? 在Windows上是用lock指令实现的,但是在Linux上怎么实现,据说有人做过实验。在Linux上实现是上面一个屏障下面一个屏障最后一条lock指令,中间才是才是你的volatile区域。

使用hsdis观察汇编码
lock指令 xxx 执行 xxx指令的时候保证对内存区域加锁

synchronized修饰方法或者代码块上锁
1.字节码层面
它内部实现,加了两条指令叫monitorenter和monitorexit,为什么有两个monitorexit,其实就是发现异常之后它会自动退出的意思。同步块synchronized产生一个monitorenter,但是产生异常之后它会自动的帮你退出所以有两条monitorexit指令。
在这里插入图片描述
2.jvm层面
是C 和 C++调用了操作系统提供的同步机制
3.os和硬件层面
X86:lock cmpxchg xxxx
硬件层面实际是lock一条指令,synchronized在字节码层面如果你是方法的话直接加了一个
ACC_SYNCHRONIZED这样一个修饰符,如果你是同步语句块的话就是monitorexit和monitorenter。
在jvm层面当它看到了那些东西之后对应的是C和C++调用了操作系统提供的同步机制是要依赖于硬件cpu的。CPU级别是使用lock指令来实现的。
要在synchronized某一块内存上加个数,把i的值从0变成1,从0变成1的这个过程如果放在cpu去执行可能有好几条指令或者不能同步,所以用lock指令。cmpxchg前面如果加了一个lock的话指的是后面的指令执行的过程当中这块区域锁定,只有我这条指令能改,其他指令时改不了的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值