详说Java内存模型(JMM)

  • Post author:
  • Post category:java




什么是Java内存模型

Java内存模型就是(Java Memory Model),它规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机就是一个完整的计算机的模型,因此这个模型自然也包含了一个内存模型——就称为Java内存模型。

通俗来说,JMM是一套多线程读写共享数据时,对数据的

可见性



有序性



原子性

的规则。



为什么提出内存模型

在硬件的发展当中,一直都存在着一个矛盾,在CPU、内存、I/O设备的速度差异。

默认的排序为:CPU > 内存 > I/O设备

所以为了平衡这三者的速度差异,就做了一写优化:

在CPU中添加寄存器,以均衡内存与CPU之间的差异;

操作系统以线程又分为复用CPU,进而均衡I/O设备与CPU的速度差异;

编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。



Java主内存与工作内存

接下来,先看看线程在执行的时候,对数据的拿去。

Java内存模型(JMM)的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程贡献的变量)存储到电脑内存和内存中取出变量的底层细节。

Java内存模型中规定了所有的变量都是存储在主内存(电脑内存)中,但每条线程还有着自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

这里的工作内存就是JMM的一个抽象概念,也就做本地内存,其中存储了该线程的读/写贡献变量的副本。


就像每个处理器内核都拥有自己私有的本地内存,同理在JMM中每个线程都拥有自己的本地内存

在不同的线程之间是无法通过直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递、而是内存共享。

Java线程间的通信采用的是共享内存的方式,线程,主内存和工作内存之间的交互。

在这里插入图片描述

在这里所说的主内存、工作内存是与Java内存区域中的堆、栈、方法区等并不是在同一个层次的内存划分,这两个基本是没有是关系的。如果非要说两个一定要面前对象起来的haul,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。



JMM三大特性



可见性

就是一个线程对共享变量的修改,另外一个线程能立刻看到,我们就称为可见性。

对于现在的多核处理器,没课CPU都是拥有自己的缓存的,但这个缓存是仅仅对它所在的处理器是可见的,但CPU缓存与主内存的数据是很难保持一致性的。

为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器就会使用

写缓冲区

来临时保存向内写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式进行刷新,也就是说写缓冲区不会即时将数据刷新到主内存中。

一句话总结一下:

多核的CPU,每个内核中都会有一个高速缓存,在每个高速缓存当中的数据在线程之间都是不可见的。



有序性

有序性指的是程序按照代码的先后顺序进行执行

为了优化性能,有时候会改变程序中语句的先后顺序。

CPU的读等待同时指令执行是CPU乱序的根源。

读指令的同时可以同时执行不影响的其他指令。


对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;

但是在多线程并发时,程序的执行就有可能出现乱序。

用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象



原子性


这是线程切换所带来的原子性问题

原子的意思就是“不可分”;

一个或多个操作在CPU执行的过程中不被中断的特性,我们就称为原子性。

原子性是拒绝多线程交互操作的,不论是多核和单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。

CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。线程切换就会导致原子性问题。

说具体一点就是:一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于MyBatis中的事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

举个例子:

一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款。要保证其有序性



举个例子


在Java当中并发程序都是基于多线程的,自然也会涉及到任务切换,任务切换的时机大多数是在时间片结束的时候。

我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条CPU指令完成。

举一个例子。i++操作:在CPU中就需要三条指令才能完成。

在这要求执行两次i ++

在这里插入图片描述

但是因为线程之间不遵守原子性,且两线程之间是不可见性的。

所以在执行的时候会出现一些问题,造成结果为1,在指令3中,最后同时给主内存当中写入数据为1。使得和我们想要的结果出现了偏差。

所以在这也说明了i++其实是线程不安全的。



并发总结


缓存可见性问题,编译优化带来的有序性问题,线程切换带来的原子性问题

。其实缓存、线程、编译优化的目的和我们写并发编程的目的是相同的,都是提高程序安全性和性能。但是技术在解决第一个问题的同时,必须会带来另外一个问题,所以在采用一项技术的同时,一定要清除它带来的问题是什么,以及如何规避。


实现可见性和有序性是volatile关键字来实现的。

实现原子性是依靠锁机制来实现的。



下一篇:===》volatile关键字实现可见性和有序性


下一篇:===》锁机制实现线程并发的原子性



版权声明:本文为weixin_45970271原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。