前言:在今天的博客内容开始之前,首先抛出一个问题,当我学完MESI一致性协议,与volatile的时候,心中起疑,这两个玩意实现的不是同一种东西吗,花了一下午时间阅读了10+篇博客包括知乎以及csdn上的一些文章,更加迷茫。。。。。
我的内心os其实是这样的,像这位匿名网友
于是,我在朋友圈随手发了一个问题,引来了三个大佬的讨论,他们分别是我曾经的同学雄哥(一个四个月下来比我多看两本书的猛男),佳杰(浙大本硕大佬,现在阿里实习)瑞瑞(我本科室友,我曾经跪着求他帮我做大作业,后来去了百度)。
一顿讨论之后,我还是保留了意见,因为我觉得大家说的点子都没问题,但是还是没有探索到这个问题的骨和肉。下午的时候我更倾向于佳杰的答案,因为我在网上看到了类似的一篇博客。这里贴出链接
https://www.zhihu.com/question/296949412/answer/747494794
可是我还是很迷惑,最后我有了自己的答案,在最后说说我自己的想法,在这之前先介绍下什么是MESI一致性协议,和JMM内存模型。
1.什么是MESI一致性协议
这里用我的大白话说,他就是一种协议,通过两位二进制记录了缓存Cache的四种状态,用来保证多个CPU对内存操作时保证可见性,一致性。
M:这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E:这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
S:这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
I:这行数据无效
它们分别为Modified、Ex'c'lusive、Shared、Invalid的简称
2.接下来我们看看MESI一致性协议是怎么工作的
上图是如果没有MESI协议。此时如果有三个线程分别由三个CPU执行,去内存中拿x=1,并且加入到自己的缓存,分别执行+1操作后,再分别写回内存,得到的答案是x=2,与预期值x=1+1+1+1=4不符合,就是因为多个cpu之间没有可见性。
而如果这里使用了MESI一致性协议,步骤回是这样的
1 CPU 1从内存中加载x变量=1到自己的缓存,此时对自己的缓存块加锁,修改状态为E,表示独占。
2CPU2从内存中加载x变量=1到自己缓存,我们的每个cpu都是有嗅探机制的,会监听总线,这个时候由于CPU2也读入了x变量,两个缓存行就一起改变状态为Shared状态。
3CPU......
4这个时候CPU1把x加载入寄存器进行+1操作后写入缓存,然后修改状态为Modified,这个时候就要去通过总线告诉别的CPU,它们的状态应该改为无效状态了,需要重新从内存读入。
5CPU1在缓存中的内存结果会在合适的时候写回内存。
6当别的CPU读入新的刷新到内存的值后,大家一起又会变成shared
这样通过MESI一致性协议,就可以做到了多个cpu操作共享资源的可见性。
JMM内存模型
什么是JMM内存模型
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
这是网上大多博客以及《并发编程的艺术》都是这样解释的
那么问题来了,它与jvm有什么区别呢,换个问法,我好好的,为什么抽象出这么一个模型来增加一些晦涩的概念呢?
JMM与JVM内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。线程,工作内存,主内存工作交互图(基于JMM规范)
上面就是一张JMM模型图,非常容易理解,屏蔽了各种细节告诉了我们JMM帮我们实现了多个线程和主内存交互时,共享资源的处理。
下面我们再看一张JVM内存模型图,这里为了方便理解JMM抽象,我还画出java线程经过操作系统调用内核线程抢夺CPU的过程。
我们会想即使你JMM模型是抽象出来的,但是你不管是主内存,还是线程工作内存都应该有真实的物理内存映射空间啊。确实。
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。
工作内存
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
综上如果把jmm和jvm对应起来应该是下面这张图
到这里我们大概知道了JMM内存模型结构,以及模型的具体映射。
2.用一段代码引出我们最早提出的问题
public class VolatileVisibilitySample { private boolean initFlag = false; private volatile boolean initFlag = false; public void refresh(){ this.initFlag = true; //普通写操作,(volatile写) String threadname = Thread.currentThread().getName(); System.out.println("线程:"+threadname+":修改共享变量initFlag"); } public void load(){ String threadname = Thread.currentThread().getName(); int i = 0; while (!initFlag){ i++; } //i++; } System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i); } public static void main(String[] args){ VolatileVisibilitySample sample = new VolatileVisibilitySample(); Thread threadA = new Thread(()->{ sample.refresh(); },"threadA"); Thread threadB = new Thread(()->{ sample.load(); },"threadB"); threadB.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); }}
这里的代码逻辑是先启动B线程,此时卡在了死循环上,这个时候启动A,改变flag的状态。这里会帮助B跳出死循环吗,执行结果是不会。这时是不是好奇了,不是讲了有MESI协议吗,讲这么多你在逗我玩?先别急,这个时候我们用volatile关键字修饰一下initflag,这个时候再看执行结果,发现线程B感知到了变化跳出了循环。所以为什么呢,看似我们有MESI协议了,为什么还要有volatile呢。这个疑问放在这里,我后面再讲(代码运行结果就不贴出来了,兄弟们自己跑下)
这里我们介绍一下JMM定义的八种原子操作
MM-同步八种操作介绍
(1)
lock(锁定)
:作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)
unlock(解锁)
:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的
变量才可以被其他线程锁定
(3)
read(读取)
:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,
以便随后的load动作使用
(4)
load(载入)
:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作
内存的变量副本中
(5)
use(使用)
:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)
assign(赋值)
:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存
的变量
(7)
store(存储)
:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,
以便随后的write的操作
(8)
write(写入)
:作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送
到主内存的变量中
然后我们结合图看一下上面代码的逻辑
首先线程B加载initFlag到自己的工作内存,然后一直循环,这个时候A也加载到内存,并且修改了值,这个时候问题来了,它会立刻写回主内存吗,答:不会。就算写回了主内存,线程A能立刻感应到吗。答:不能。
这个时候疑惑来了,我的MESI被你吃了吗。经过我的查询,为什么加了volatile就可以了,我们看看volatile帮我们做了什么
volatile实现的两条原则(保证可见性和禁止指令重排序)
LOCK前缀指令会引起处理器缓存写回到内存。
一个处理器的缓存回写到内存会导致其他处理器的缓存无效
这两句话摘选自《并发编程的艺术第十页》,只是两个标题,还有具体的解释,我按照我的想法对它进行解释,就是volatile会进行一些操作,导致唤醒MESI协议的使用,这里我们明白了,原来JMM是通过volatile保证线程间可见的根本原因是以为,它触发了MESI的机制,以前我一直认为它既然是多个CPU之间的底层协议,纳闷他应该是默认实现的吧,然而我们的第一种情况的代码告诉我们,并没有。
当然这只是我自己的看法,博客就是这样,谁敢保证自己说的一定对呢。我看了十几篇博客,大家都各有说辞,不过,我为了验证自己的理论所经历的过程,收集支撑自己想法的证据所获得的收益不仅仅是一个答案这么简单。
下篇博客我准备趁热打铁讲解一下volatile,为什么只能帮JMM模型实现可见和有序,不能保证原子性。以及JMM的原子性如何保证。
,