Java之JVM的深入探索(二)–内存模型、可见性、指令重排序

作者 : 开心源码 本文共2570个字,预计阅读时间需要7分钟 发布时间: 2022-05-12 共194人阅读

接着上一篇我们已经理解到了JVM的基本运行流程以及内存结构,在对JVM上已经有了一个初步通俗的认知,其中在上一篇详情JVM的时候我们详情了JVM的内存空间,本文将理解到java中变量的可见性及不同的java指令在并发访问时可能发生的指令重排的情况,并且针对内存空间的理解上我们在来探索一下JVM 的内存模型,接下来让我们一起进一步探索:

Java变量的可见性:

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其余线程来说是可以立即得知的。

Java中有一个关键字volatile,到这里先简单的详情下volatile:

volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比用synchronized所带来的庞大开销,倘若能恰当的正当的用volatile,自然是美事一桩。

那么,它有什么使用呢?

这个答案其实就在上述java线程间通信机制中,我们想象一下,因为工作内存这个中间层的出现,线程1和线程2必然存在推迟的问题,例如线程1在工作内存中升级了变量,但还没刷新到主内存,而此时线程2获取到的变量值就是未升级的变量值,又或者者线程1成功将变量升级到主内存,但线程2仍然用自己工作内存中的变量值,同样会出问题。不论出现哪种情况都可能导致线程间的通信不能达到预期的目的。

例如以下例子:

//线程1
booleanstop =false;
while(!stop){ doSomething(); }
//线程2
stop =true;

这个经典的例子表示线程2通过修改stop的值,控制线程1中断,但在真实环境中可能会出现意想不到的结果,线程2在执行之后,线程1并没有立刻中断甚至一直不会中断。出现这种现象的起因就是线程2对线程1的变量升级无法第一时间获取到。

但这一切等到Volatile出现后,再也不是问题,Volatile保证两件事:
线程1工作内存中的变量升级会强制立即写入到主内存;
线程2工作内存中的变量会强制立即失效,这使得线程2必需去主内存中获取最新的变量值。
所以这就了解了Volatile保证了变量的可见性,由于线程1对变量的修改能第一时间让线程2可见。

Java指令重排序:

到这里,关于指令排序我们先看下面一段代码:
inta =0;booleanflag =false;|
//线程1
publicvoidwriter(){
a =1;
flag =true;
}

//线程2
publicvoidreader(){
if(flag) {
inti= a+1;
……
}
}

由代码我们不难看出:线程1依次执行a=1,flag=true;线程2判断到flag==true后,设置i=a+1,根据代码语义,我们可能会推断此时i的值等于2,由于线程2在判断flag==true时,线程1已经执行了a=1;所以i的值等于a+1=1+1=2;但真实情况却不肯定如此,引起这个问题的起因是线程1内部的两条语句a=1;flag=true;可能被重新排序执行,如图:

这就是指令重排序的简单演示,两个赋值语句虽然他们的代码顺序是一前一后,但真正执行时却不肯定按照代码顺序执行。你可能会说,有这个指令重排序那不是乱套了吗?我写的程序都不按我的代码流程走,这怎样玩?这个你可以放心,你的程序不会乱套,由于java和CPU、内存之间都有一套严格的指令重排序规则,哪些可以重排,哪些不能重排都有规矩的。下列流程演示了一个java程序从编译到执行会经历哪些重排序:

在上图这个流程中第一步属于编译器重排查,编译器重排序会按JMM的规范严格进行,换言之编译器重排序一般不会对程序的正确逻辑造成影响。第二、三步属于解决器重排序,解决器重排序JMM就不好管了,怎样办呢?它会要求java编译器在生成指令时加入内存屏障,内存屏障是什么?你可以了解为一个不透风的保护罩,把不能重排序的java指令保护起来,那么解决器在遇到内存屏障保护的指令时就不会对它进行重排序了。关于在哪些地方该加入内存屏障,内存屏障有哪些种类,各有什么作使用,可以参考JVM规范相关资料。

下面详情一下在同一个线程中,不会被重排序的逻辑:

这三种情况中,任意改变一个代码的顺序,结果都会大不相同,对于这样的逻辑代码,是不会被重排序的。注意这是指单线程中不会被重排序,假如在多线程环境下,还是会产生逻辑问题,例如前面举的例子。

JVM内存模型:

首先, 我们这里把Java堆称为主内存,并且Java堆是线程共享的,但是每一个线程都是有自己私有的内存空间的,这里我们暂称为每一个线程自己的工作空间。有了这个前提后我们想一想在java中一个线程要想另外一个线程进行通信应该怎样做? 说的更加确切一点就是一个java线程对于一个变量的升级是怎样通知到另外一个线程的呢? 有上一篇文章我们理解到java当中的?实例对象、数组元素都存放在java堆中的.假如线程1要向线程2通信,肯定会经过下图相似的流程:

简单的解释下该图的:
线程1将自己工作内存中的x升级为1并刷新到主内存中;
线程2从主内存中读取到变量x=1,并且升级到自己的工作内存中,因而,线程2读取的x就是线程1更细后的值。
从上图可知,java线程之间的通信都需要经过主内存,而主内存与工作内存之间的交互则需要java内存模型(JMM)管理器,下图中我们可以看到JMM是如何管理主内存与工作内存的:

需理解一:
当线程1需要将一个升级后的变量值刷新到主内存时,需要经过以下两个步骤:
????线程1工作内存执行store操作;
????主内存执行write操作;
????完成以上这两步就可将工作内存中的变量值刷新到主内存,即线程1工作内存和主内存的变量值保持一致;

需理解二:? ??
当线程2需要从主内存中读取变量的最新值时,同样需要经过两个步骤:
????主内存执行read操作,将变量值从主内存中读取出来;
????线程2工作内存执行load操作,将读取出来的变量值升级到本地内存的副本;
? ? 完成以上这两步,线程2的变量和主内存的变量值就保持一致了。

完成以上四个步骤:就实现了一个java线程要向另外一个线程进行通信。接着我们在探索一下java中变量可见性的问题:

到此,本文所述已完毕

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Java之JVM的深入探索(二)–内存模型、可见性、指令重排序

发表回复