在学习Java内存模型之前,有几个知识点必须先了解一下。
1. cpu和物理内存的读写速度差会导致什么问题?如何解决?
2. 计算机内存模型是什么?为什么需要计算机内存模型?
最后再了解:
3. 什么是JMM?
cpu和物理内存的读写速度差会导致什么问题?如何解决?
计算机的每条指令,都是通过cpu来执行的,执行过程中,大多情况下都需要与内存打交道。在早些时候,cpu的运算速度和内存的运算速度相差无几,于是两者过的相安无事。但是随着cpu的不断发展,cpu的计算速度远远超过了内存。所以就导致每次cpu都要在等待内存上耗费很长时间。
于是就诞生了高速缓存这一中间人,其实也就是我们平常所说的缓存。它的作用时:在cpu进行计算时,先将所需的数据从内存拷贝一份到高速缓存中,cpu在获取数据时,就直接从缓存中获取,写数据的时候,也可以直接往高速缓存中写入。运算结束时,再将缓存中的数据刷新到内存中即可。
举个例子,有一份工作需要张三和李四配合进行,张三负责工作前半部分,李四负责工作后半部分,起初两者都是新手,干活速度不相上下,于是这个工作刚好能衔接上。但是随着时间的积累,李四的干活速度越来越快(cpu在升级),而张三的干活速度依旧如此(内存的速度在原地踏步),所以导致李四干完了,需要浪费很多时间在等待张三上。 所以呢,公司为了这个问题,又招了王五作为协调员,又招了n个张三来做底层工作。协调员这边负责将n个张三完成的工作缓存起来(高速缓存的作用),等到李四开始工作时,直接与协调员对接即可。
计算机内存模型是什么?为什么需要计算机内存模型?
咱们这先提一下计算机缓存,计算机缓存分一级缓存(L1 cache)、二级缓存(L2 cache)、三级缓存(L3 cache)。当然不是所有计算机都配有这三级,也是看配置的。
在cpu执行时,会先从一级缓存中获取数据,如果没有再从二级缓存中获取数据,如果还是没有,则会去三级缓存或者内存中获取数据。
对于多核cpu来说,每个核都会有自己的一级缓存,二级缓存可能是共享的,也可能是单独的,而三级缓存一般来说都是共享的。
说完了计算机缓存,咱们再聊一下计算机的单线程和多线程,单核与多核的问题,这将对计算机内存模型产生至关重要的影响。
我们可以分下面几种情况来考虑:
单线程(无论单核还是多核)
单线程执行,就不用考虑资源竞争的问题了,反正数据都是按顺序读的,按顺序写的,不会存在并发修改等并发问题,这种时候也就不需要对内存数据进行什么保护措施,数据是绝对不可能乱的。单核多线程
单核是可以开启多线程操作的,但不要误以为这是并行,你肉眼看着多个任务同时进行,其实底层操作系统还是按着串行但方式在运行,操作系统是以时间片为单位控制着每个线程的挂起和执行。
举例来说,比如当下有a、b、c三个线程,操作系统给a分了10个时间片,于是a执行了10个时间片后,就被操作系统挂起了,紧接着又给b分了10个时间片,b执行完后也被挂起了,如此反复,因为一个时间片大概10ms左右,所以以人肉眼的角度来观察多个任务的执行情况,其实是看不出这其实是在串行执行。
所以,这也解释了,这种情况下,其实也不需要对内存数据做什么保障,串行已经解决了一切脏数据问题。多核多线程
上面也说到了,每个核心,是有自己对一级缓存的,所以当core1上当线程1修改了该内核下当L1缓存后,core2上当线程2修改了该内核下当L1缓存,这时两者当缓存数据就对不上了,如果这时候需要对这份数据进行其它计算,那就会导致计算错误。这其实也就是我们经常说的缓存一致性问题。
除了上述多核多线程的情况会导致缓存不一致问题之外,如果考虑硬件方面的话,还有其它情况,我这里就说一点:
- 指令重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
假设现在有如下两句代码要执行:
a = 1; a = 2;
很明显,先执行a=1
或a=2
,对于a的结果值来说是显然不同的两种情况。当然,在单线程情况下,操作系统会在遵守原有的串行逻辑的语义下,对指令进行重排序,人话来说就是:我不会让你对结果变的!
但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑到,所以会因此而产生数据不一致的情况。
那这些问题操作系统怎么解决呢?内存模型!
简单的来说,计算机内存模型定义了两种解决方式:
- 内存屏障
- 限制处理器做优化
什么是JMM?
- Java内存模型是一个抽象的概念,并非真实存在。
- JMM定义了线程和主内存之间的关系:
- 线程之间的共享变量存储在主内存中。
- 每个线程都有一个私有的本地内存,其中保存了该线程使用到的共享变量的主内存副本拷贝。
- JMM屏蔽了操作系统和硬件的内存访问差异,所以Java才得意在各个平台上达到访问内存的一致性。
- 重点:主内存和线程私有的工作内存,与JVM的堆栈不是一回事。主内存应该是指内存条,而工作内存应该是指寄存器和高速缓存。
JMM的核心原则
多线程的原子性、可见性、有序性。
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
原子性
- 原子性是指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
可见性
- 可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更
- Java中普通的共享变量不保证可见性,因为其修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读"
- 缓存优化或者硬件优化或指令重排以及编辑器的优化都可能导致一个线程修改不会立即被其他线程察觉
- Java提供volatile保证可见性:写操作立即刷新到主内存,读操作直接从主内存读取
- Java同时还可以通过加锁的同步性间接保证可见性:synchronized和Lock能保证同一时刻只有一个线程获取锁并执行同步代码,并在释放锁之后将变量的修改刷新到主内存中
有序性
- 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行,但为了提供性能,编译器和处理器通常会对指令序列进行重新排序
- 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读"
参考文章:
https://mp.weixin.qq.com/s/n0U2IJwhT3OAp_EwRdzYIA
https://www.zybuluo.com/kiraSally/note/850631#3happends-before