从硬件缓存模型到Java内存模型原理浅析

参考Google的这个问题what is a store bufferbr>一、硬件方面的问题
1、背景
在现代系统的CPU中,所有的内存访问都是通过层层缓存进行的。CPU的读/写(以及指令)单元正常情况下甚至都不能直接与内存进行访问,这是物理结构决定的。CPU和缓存进行通信,而缓存才能与内存进行通信。处理器保证从系统内存中读取或者写入一个字节是原子的,但是复杂的内存操作处理器是不能保证其原子性的,比如跨总线操作、跨多个缓存行和跨页表的访问。但是处理器提供了总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
硬件缓存模型如下图所示:
解释:具体这些会在MESI协议里面讲,先有个概念
(1) CPU就是CPU
(2)Store Bufferes存储缓存
(3)Cache 就是代表高速缓存
(4)Invalidate Queues 无效队列
(5)Memory 内存

总结
我们看到硬件为了解决原子性,使用了总线锁和缓存锁,缓存锁是基于缓存一致性协议实现的。缓存一致性协议带来了指令执行顺序问题,影响了多核处理器之间的可见性。因为硬件无法知道我们这些软件数据在执行时的指令顺序,所以硬件就制定了这样一套硬件规则来满足硬件需求,提供Memory Barrier来解决方案来应对软件可能发生的问题,具体需要我们软件自己去实现。
二、软件层面的问题(JAVA)
我们在编写并发程序时,也会出现问题原子性问题、可见性问题。
Java如何实现原子操作
在Java中可以通过锁和循环CAS的方式实现原子操作。
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。从Java 1.5开始,JDK并发包提供了一些类支持原子操作。
CAS实现原子操作存在的问题
1)ABA问题。因为CAS需要在操作值得时候,检查值没有变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,由变成了A,那么使用CAS允许检查时会发现它的值没有发生变化,但是实际却发生了变化。ABA问题的解决思路就是使用版本 ,每次变量更新的时候版本 加1,那么A-B-A就会变成1A-2B-3A.从Java1.5开始,JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将引用和该标志的值设置为给定的更新值。
2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证一个共享变量的原子操作。
使用锁机制来实现原子性:锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制。
可见性问题
Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员。
Java内存模型的抽象结构
在Java中,所有实例域、静态域和数组元素都存在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数和异常处理器参数不会再线程之间共享,他们不会存在内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓存区、寄存器以及其他的硬件和编译优化。如下图所示。

从硬件缓存模型到Java内存模型原理浅析
重排序的规则;as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变。编译和处理器都必须遵循as-if-serial语义。但是如果操作之间没有数据依赖关系,这些操作就可以被重排序。
对于处理器的(内存和指令)重排序,JMM的处理器重排序规则会要求Java编译器生成指令时,插入特定类型的内存屏障指令。JMM把内存屏障分为4类:
屏障类型 指令示例 说明
LoadLoad Barries Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及后续所有装载指令的装载
StoreStore Barries Store1;LoadLoad;Store2 确保Store1数据对其它处理器可见(刷新到内存)先于Store2机后续所有指令的存储
LoadStore Barries Load1;LoadLoad;Store2 确保Load1数据的装载先于Store2及所有后续的存储指令的刷新到内存
StoreLoad Barries Store1;LoadLoad;Load2 确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2及后续所有指令的装载

happens-before(JMM可见性的保证)
JSR-133使用happens-before的概念来指定连个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性。
happens-before关系定义
(1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作前面。这是JMM对程序员的保证。
(2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before的指向顺序来执行。如果重排序之后的执行结果,与按happens-before关系的执行结果一致,这种重排序并不非法(也就是说,JMM允许这种重排序)。这是JMM对重排序指定的规则,只要不改变程序的执行结果(单线程和正确同步的线程),怎么优化都行。
happens-before规则(满足规则即满足可见性)
1)程序顺序规则:一个线程中的每个操作,happens-before与该线程中的任意后续操作。
2)监视器锁规则:对一个锁的解锁,happens-before与随后对这个锁的加锁。
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()规则:如果线程A 执行操作ThreadB.start()(线程B启动),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6)join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功的返回。
总结:Java提供了锁和CAS来保证原子性操作,通过JMM的规则来禁止一些重排序,通过JMM的happens-before规则来保证内存的可见性。我们可以看到规则里面有一些关键字,volatile(通过内存屏障)、锁保证了可见性,我们在下面的章节详解—————————以下是个人理解:我们结合硬件缓存模型来看,其实JMM是对处理器缓存模型的一种实现,硬件实现了最终缓存在一致性的方案,并提供了强一致性缓存的解决方案(内存屏障的指令),JMM实现了这个方案,在我们需要的时候(插入内存屏障)提供强大的可见性保证,不需要时遵循硬件的优化策略(可以进行指令重排序优化,提高执行性能)。

文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树首页概览91322 人正在系统学习中 相关资源:实例讲解分布式缓存软件Memcached的Java客户端使用-其它代码类…

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2019年2月22日
下一篇 2019年2月22日

相关推荐