JVM/JMM/计算机网络

1.现在计算机往往是多核心的,每个核心下会有高速缓存。高速缓存的诞生是由于CPU与内存的速度存在差异,L1和L2缓存一般是每个核心独占一份的。可见行
2.为了让CPU提高运算效率,处理器可能会对输入的代码进行乱序执行,也就是所谓的指令重排序。有序性
3.一次对数值的修改操作往往是非原子性的(比如i++实际上在计算机执行时就会分成多个指令)。原子性
在永远单线程下,上面将的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器/runtime/处理器都必须遵守as-if-serial语义,遵守as-if-serial意味着他们不会对数据依赖关系的操作 做重排序。
CPU为了效率,有了高速缓存,有了指令重排序等等,整块架构都变得复杂了。我们写程序肯定也想要充分利用CPU的资源,于是我们使用起了多线程。
多线程意味着并发,需要考虑线程安全问题
1.缓存数据不一致:多个线程同时修改共享变量,CPU核心下的高速缓存是不共享的,那多个cache与内存之间的数据同步要怎么处理?
2.CPU指令重排序在多线程下回导致代码在非预期下执行,最终会导致结果存在错误情况。
针对缓存不一致的问题,CPU也有解决办法:
1.使用总线锁:某个核心在需改数据的过程中,其他核心均无法修改内存中的数据。类似于独占内存的概念,只要有CPU在修改,那别的CPU就i要等待当前CPU释放。
2.缓存一致性协议(MESI协议,其实协议有很多):缓存一致性协议我认为可以理解为缓存锁,它针对的是缓存行(cacheIine)进行加锁,所谓的缓存行其实就是高速缓存,存储的最小单位。MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的对象状态(修改、共享、独占、无效)
如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取,如果变量是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改。
如果是修改,说明当前CPU正在修改改变量的值,同时会像其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享变成invalid),会把当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)
如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。
其实MESI协议做的就是判断对象状态,根据对象状态做不同的策略,关键就在与某个CPU在对数据进行修改时,需要同步通知CPU,表示这个数据被我修改了,你们不能用了。
比较与总线锁,MESI协议的锁粒度更小,性能略高。
CPU优化: 优化的思路就是从同步变异步
在修改是会同步高速其他CPU,而现在则把最新修改的值写到 store buffer 中,并通知其他CPU记得要该状态,随后CPU就直接返回干其他事了。等收到其他CPU发过来的响应消息,再将数据更新到高速缓存中。其他CPU接收到invalid通知时,也会把接受到的雄安锡放入 invalid queue就会直接返回告诉修改数据的CPU已经将状态置为invalid
异步又会带来新的问题,CPU修改完A值,写道store buffer 中 CPU就可以干其他事了,那如果该CPU又接收指令需要修改A值,但上一次修改的值还在 store buffer中,没修改至高速缓存呢。所以CPU在读取的时候,需要去Store buffer 看看存不存在,存在则直接取,不存在才读主存的数据。store Forwarding。相同的核心对数据进行读写,由于异步,很可能会导致二次读取的还是旧值,所以首先读store buffer,异步化会导致相同核心读写共享变量有问题,那当然也会导致不同核心读写共享变量有问题。
CPU1修改了A值已经把修改后的值写道store buffer 并通知CPU2对该值进行invalid操作,而CPU2可能还没有收到invalid通知,就去做了其他的操作,导致CPU2读到的还是旧值。即便CPU2收到了invalid通知,但是CPU1的值还没有写道主存,那CPU2再次向主存读取的时候还是旧值。
变量之间很多时候具有相关性 a=1;b=0;b=a 这对CPU是无感知的。
由于CPU对缓存一致性协议的进行的异步优化,很可能导致后面的指令查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序)这种现象很多时候被称作CPU乱序执行
为了解决乱序问题(也可以理解为可见性问题,修改完没有及时同步到其他CPU)又引出了内存屏障概念。
内存屏障:可以分为三种类型:写屏障,读屏障以及全能屏障。屏障可以简单理解为:在操作数据的时候,往数据插入一条特殊指令。只要遇到这条指令那前面的操作都得完成。
写屏障:CPU当发现写屏障的指令时,会把该指令之前存在与 store buffer 所有写指令刷入高速缓存, 通过这种方式就可以让CPU,达到写操作可见性的效果。

读屏障:CPU当发现读屏障的指令时,会把该指令之前存在与invalid queue所有指令都处理掉,通过这种方式就可以确保当前CPU的缓存状态时准确的,达到读操作一定是读取最新的效果。
由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作,Java封装了一套规范,这套规范就是Java内存模型
Java内存模型希望屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果,目前解决多线程存在的原子性,可见性以及有序性的问题。

并发问题产生的三大根源是「可见性」「有序性」「原子性」
可见性:CPU架构下存在高速缓存,每个核心下的L1/L2高速缓存不共享(不可见)
有序性:主要有三部分可能导致打破(编译器和处理器可以在不改变「单线程」程序语义的情况下,可以对代码语句顺序进行调整重新排序
编译器优化导致重排序(编译器重排)
指令集并行重排序(CPU原生重排)
内存系统重排序(CPU架构下很可能有store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)
原子性:Java的一条语句往往需要多条 CPU 指令完成(i++),由于操作系统的线程切换很可能导致 i++ 操作未完成,其他线程“中途”操作了共享变量  i ,导致最终结果并非我们所期待的。
在CPU层级下,为了解决「缓存一致性」问题,有相关的“锁”来保证,比如“总线锁”和“缓存锁”。
总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。
缓存锁是锁缓存行(cache line),其中比较出名的是MESI协议,对缓存行标记状态,通过“同步通知”的方式,来实现(缓存行)数据的可见性和有序性
但“同步通知”会影响性能,所以会有内存缓冲区(store buffer/invalid queue)来实现「异步」进而提高CPU的工作效率
引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,平日大多数情况下是可以享受「异步」带来的好处的,但少数情况下,需要强「可见性」和「有序性」,只能"禁用"缓存的优化。
“禁用”缓存优化在CPU层面下有「内存屏障」,读屏障/写屏障/全能屏障,本质上是插入一条"屏障指令",使得缓冲区(store buffer/invalid queue)在屏障指令之前的操作均已被处理,进而达到 读写 在CPU层面上是可见和有序的。
不同的CPU实现的架构不一样,Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值