Java内存模型之JMM
# Java 内存模型之 JMM
# Java 内存模型 Java Memory Model
计算机存储结构,从本地磁盘到主存到 CPU 缓存,也就是从硬盘到内存,到 CPU。一般对应的程序的操作就是从数据库查数据到内存然后到 CPU 进行计算

因为有这么多级的缓存 (cpu 和物理主内存的速度不一致的), CPU 的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题

Java 虚拟机规范中试图定义一种 Java 内存模型(java Memory Model,简称 JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。计算机硬件底层的内存结构过于复杂,JMM 的意义在于避免程序员直接管理计算机底层内存,用一些关键字 synchronized、volatile 等可以方便的管理内存。
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成

- 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值
- 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝
JMM (Java 内存模型) 本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中 (尤其是多线程) 各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则: JMM 的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
- 通过 JMM 来实现线程和主内存之间的抽象关系。
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
JVM 和 JMM 之间的关系:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来:
- 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
- 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存
# 内存交互
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,每个操作都是原子的
非原子协定:没有被 volatile 修饰的 long、double 外,默认按照两次 32 位的操作
- lock:作用于主内存,将一个变量标识为被一个线程独占状态(对应 monitorenter)
- unclock:作用于主内存,将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定(对应 monitorexit)
- read:作用于主内存,把一个变量的值从主内存传输到工作内存中
- load:作用于工作内存,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- use:作用于工作内存,把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令
- assign:作用于工作内存,把从执行引擎接收到的一个值赋给工作内存的变量
- store:作用于工作内存,把工作内存的一个变量的值传送到主内存中
- write:作用于主内存,在 store 之后执行,把 store 得到的值放入主内存的变量中
# 三大特性
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
# 可见性
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM 规定了所有的变量都存储在主内存中。

Java 中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现 "脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程脏读:如果没有可见性保证
主内存中有变量 x,初始值为 0 线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值 线程 A 将更新后的 x 值回刷到主内存的时间是不固定的 刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会变成 x=1

# 原子性
原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响
定义原子操作的使用规则:
- 不允许 read 和 load、store 和 write 操作之一单独出现,必须顺序执行,但是不要求连续
- 不允许一个线程丢弃 assign 操作,必须同步回主存
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作
- 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁,lock 和 unlock 必须成对出现
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新从主存加载
- 如果一个变量事先没有被 lock 操作锁定,则不允许执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量
- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)
# 有序性
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。 但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生 "脏读",简单说, 两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排序时必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
# 多线程对变量的读写过程
由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存 (有些地方称为栈空间),工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作 (读取赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信 (传值) 必须通过主内存来完成,其简要访问过程如下图:

JMM 定义了线程和主内存之间的抽象关系
- 线程之间的共享变量存储在主内存中 (从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读 / 写共享变量的副本 (从硬件角度来说就是 CPU 的缓存,比如寄存器、L1、L2、L3 缓存等)
总结:
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本 (主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写 (不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行 (同级不能相互访问)
# 多线程先行发生原则之 happens-before
在 JMM 中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在 happens-before 关系。
如 x = 5 线程 A 执行,y = x 线程 B 执行,称之为:写后读
y 是否等于 5 呢?
如果线程 A 的操作(x= 5)happens-before (先行发生) 线程 B 的操作(y = x), 那么可以确定线程 B 执行后 y = 5 一定成立;如果他们不存在 happens-before 原则,那么 y = 5 不一定成立。这就是 happens-before 原则的威力。-------------------> 包含可见性和有序性的约束
如 1+2+3 = 3+2+1
# 次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量 X 赋值为 1,那后面一个操作肯定能知道 X 已经变成了 1。
# 锁定规则
一个 unLock 操作先行发生于后面 ((这里的 “后面” 是指时间上的先后)) 对同一个锁的 lock 操作;
# volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的 “后面” 同样是指时间上的先后。
# 传递规则
如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;
# 线程启动规则 (Thread Start Rule)
Thread 对象的 start() 方法先行发生于此线程的每一个动作
# 线程中断规则 (Thread Interruption Rule)
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过 Thread.interrupted( ) 检测到是否发生中断
# 线程终止规则 (Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread::join() 方法是否结束、 Thread::isAlive() 的返回值等手段检测线程是否已经终止执行。
# 对象终结规则 (Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始,对象没有完成初始化之前,是不能调用 finalized () 方法的。
# 案例
private int value = 0;
public void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}
2
3
4
5
6
7
8
9
假设存在线程 A 和 B,
线程 A 先(时间上的先后)调用了 setValue (1),
然后线程 B 调用了同一个对象的 getValue (),
那么线程 B 收到的返回值是什么?
我们就这段简单的代码一次分析 happens-before 的规则(规则 5、6、7、8 可以忽略,因为他们和这段代码毫无关系):
- 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
- 两个方法都没有使用锁,所以不满足锁定规则;
- 变量不是用 volatile 修饰的,所以 volatile 变量规则不满足;
- 传递规则肯定不满足;
所以我们无法通过 happens-before 原则推导出线程 A happens-before 线程 B,虽然可以确认在时间上线程 A 优先于线程 B 指定,但就是无法确认线程 B 获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?
- 把 getter/setter 方法都定义为 synchronized 方法
- 把 value 定义为 volatile 变量,由于 setter 方法对 value 的修改不依赖 value 的原值,满足 volatile 关键字使用场景