Java内存模型--JMM
- 一、JMM是什么?
- 二、Happens-Before原则
- 三、JMM有什么用?
一、JMM是什么?
JMM,全拼Java Memory Model,翻译过来就是Java内存模型。 那么,我们不禁思索,Java内存模型有什么用,是用来做什么的呢?
带着这个问题,学过计算机组成原理的同学应该知道,CPU中寄存器的访问速度很快,内存的访问速度相较来说,比较慢,为了协调CPU中寄存器与内存的访问速度差异,设计了L1,L2,L3
三级缓存,如下(以笔者电脑为例):
有了缓存之后,我们尽可能操作缓存中的数据,接下来问题来了,如果缓存中的数据被修改了,会造成缓存与内存中数据不一致。为了解决这个问题,提出了缓存一致性策略,这个东西不是本文的重点,文末会给出CPU缓存一致性的方法。
整体效果图如下:
每个CPU核有自己独立的L1,L2
缓存,共用L3
缓存,通过缓存一致性将L3
缓存与主存中的数据保持一致。
但是,不同系统下的指令集与时钟都不一致,Java语言要能够兼容不同系统下的读写,Java完全可以这么做:Java-windows
,java-linux
,java-macos
,发行多个版本,然后维护多个版本,但实际上并没有这么做,而是做了一种约定, 用于屏蔽底层不同操作系统与硬件之间的读写差异。 注意,我们说的约定,这个约定就是JMM
,也就是Java内存模型。
Java内存模型有三个特点:
- 可见性:一个线程对共享变量的修改,其他线程立即可见。(线程A修改数据之后写回内存,线程B读取)
- 原子性:一个线程在操作的时候不允许其他线程打断,这个操作要么都完成,要么都不完成。
- 有序性:只要程序最终结果与串行执行结果一致,那么将允许指令重排序。有序性只保证单个线程的正确性。
Q:为什么说的是线程呢?
A:上面图画的明明时CPU,其实是一个意思,一个CPU在一个时间片内只能执行一个线程。
Q:为什么是共享变量而不是主存?
A:Java中变量存储在主存中。
上面图片抽象成Java多线程如下:
JMM规定,任何线程不能直接操作主内存中的变量,应该先将主内存的变量复制一份到本地独立栈空间内,然后进行操作,操作完之后,将变量写回主内存,线程B再读取主存中的值就是最新的了。
上面操作只能保证单线程下正确访问,多线程并发下不能保证一致性。
JMM定义了 8 8 8 中操作来完成内存间交互:
操作 | 说明 |
---|---|
lock | 作用于主内存变量,把一个变量标识为一个线程独占的状态 |
unlock | 作用于主内存变量,把处于锁定的变量释放出来 |
read | 读,作用于主内存变量,将主内存变量读到线程本地内存中 |
load | 加载,将read操作读到的变量存入本地内存的副本中 |
use | 作用于本地内存变量,将变量的值交给执行引擎 |
assign | 作用于本地内存变量,将从执行引擎读到的值赋值给本地内存变量 |
store | 存储,作用于主内存,将本地内存变量的值送到主存中 |
write | 写入,作用于主内存,store 的值赋值给主内存变量 |
后面六个是原子操作,但是组合起来不是原子操作,例如read/load
组合起来不是原子的,read
的中间可能有别的线程再read/load
。
例如:两个线程执行同一个代码快:
x = 1
x ++
线程A, B
分别读取
1
1
1 到自己的本地内存中,A
先执行了x++
,将主存中值修改为2
,但是线程B此时并不知道,因此它也执行了x++
,主存中的值还是2
,与我们想象中的3
不同。
二、Happens-Before原则
Happens-Before原则定义了前一种操作的结果对后一种操作的结果是可见的。
比如说:操作1 happens-before
操作2, 即使操作1和操作2不在一个线程内执行,JMM也会保证操作1的结果对操作2是可见的。
具体来说, happens-before有八个原则:
- 次序原则:一个线程内,写在前面的操作先行发生于写在后面的操作,例如
x=3, y=x+1
, 第一条语句肯定先行发生于第二条语句。 - 锁定原则:解锁操作
happens-before
加锁操作 volatile
原则:对一个volatile
变量的写操作,一定先行发生于对该变量的读操作。传递原则
:A happens-before B, B happens-before C, 那么 A happens-before C线程启动原则
:线程对象的start
方法先行发生于线程内的具体操作。- 线程中断规则:interrupt方法先行发生于isInterrupted()为true的代码块内的方法。
- 线程终止规则:线程内所有操作都优先发生于线程终止操作。可以用isAlive判断是否存活
- 对象终结规则:一个对象初始化完成先行发生于它的finalize()方法
JMM抽象了这个原则来解决指令重排序,有了这个原则,不需要所有的变量都加上volatile 来禁止指令重排序了,简化代码编辑
三、JMM有什么用?
有了JMM规范,我们不需要在任何变量前都加volatile
,synchronized
,lock
,提高系统执行效率。
JMM高并发场景下三个特性的落地实现:
- 原子性:
lock, unlock
,synchronized
保证了原子性 - 可见性:
synchronized
,lock, unlock
,volatile
保证了数据的可见性。 - 有序性:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。·
volatile
关键字可以禁止指令重排序。