小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)( 三 )


CPU层面的内存屏障CPU内存屏障主要分为以下三类:写屏障(Store Memory Barrier):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存 , 简单来说就是使得写屏障之前的指令的结果对写屏障之后的读或者写是可见的 。 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行 。 配合写屏障 , 使得写屏障之前的内存更新对于读屏障之后的读操作是可见的 。 全屏障(Full Memory Barrier):确保屏障前的内存读写操作的结果提交到内存之后 , 再执行屏障后的读写操作 。 这些概念听起来可能有点模糊 , 我们通过将上面的例子改写一下来说明:
package com.zwx.concurrent;public class ReSortDemo {int value;boolean isFinish;void cpu0(){value = http://kandian.youth.cn/index/10;//S->I状态 , 将value写入store bufferes , 通知其他CPU当前value的缓存失效storeMemoryBarrier();//伪代码 , 插入一个写屏障 , 使得value=http://kandian.youth.cn/index/10这个值强制写入主内存isFinish=true;//E状态}void cpu1(){if (isFinish){//trueloadMemoryBarrier();//伪代码 , 插入一个读屏障 , 强制cpu1从主内存中获取最新数据System.out.println(value == 10);//true}}void storeMemoryBarrier(){//写屏障}void loadMemoryBarrier(){//读屏障}}通过以上内存屏障 , 我们就可以防止了指令重排序 , 得到我们预期的结果 。 总的来说 , 内存屏障的作用可以通过防止 CPU 对内存的乱序访问来保证共享数据在多线程并行执行下的可见性 , 但是这个屏障怎么来加呢?回到最开始我们讲 volatile关键字的代码 , 这个关键字会生成一个 lock 的汇编指令 , 这个就相当于实现了一种内存屏障 。 接下来我们进入volatile原理分析的正题
JVM层面在JVM层面 , 定义了一种抽象的内存模型(JMM)来规范并控制重排序 , 从而解决可见性问题 。
JMM(Java内存模型)JMM全称是Java Memory Model(Java内存模型),什么是JMM呢?通过前面的分析发现 , 导致可见性问题的根本原因是缓存以及指令重排序 。而JMM 实际上就是提供了合理的禁用缓存以及禁止重排序的方法 。 所以JMM最核心的价值在于解决可见性和有序性 。 JMM属于语言级别的抽象内存模型 , 可以简单理解为对硬件模型的抽象 , 它定义了共享内存中多线程程序读写操作的行为规范 , 通过这些规则来规范对内存的读写操作从而保证指令的正确性 , 它解决了CPU 多级缓存、处理器优化、指令重排序导致的内存访问问题 , 保证了并发场景下的可见性 。 需要注意的是 , JMM并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度 , 也没有限制编译器对指令进行重排序 , 也就是说在JMM中 , 也会存在缓存一致性问题和指令重排序问题 。 只是JMM把底层的问题抽象到JVM层面 , 再基于CPU层面提供的内存屏障指令 , 以及限制编译器的重排序来解决并发问题 。
JMM抽象模型结构JMM 抽象模型分为主内存、工作内存;主内存是所有线程共享的 , 一般是实例对象、静态字段、数组对象等存储在堆内存中的变量 。 工作内存是每个线程独占的 , 线程对变量的所有操作都必须在工作内存中进行 , 不能直接读写主内存中的变量 , 线程之间的共享变量值的传递都是基于主内存来完成 , 可以抽象为下图:
小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)JMM如何解决可见性问题从JMM的抽象模型结构图来看 , 如果线程A与线程B之间要通信的话 , 必须要经历下面2个步骤 。 1)线程A把本地内存A中更新过的共享变量刷新到主内存中去 。 2)线程B到主内存中去读取线程A之前已更新过的共享变量 。 下面通过示意图来说明这两个步骤:


推荐阅读