详说Java内存模型(JMM)

什么是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关键字实现可见性和有序性

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

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值