Java 虚拟机之 JMM

590 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情

什么是 JMM

什么是 JMM 呢?一言蔽之,JMM 就是 Java 内存模型。

Java 虚拟机规范中定义了一种 Java 内存 模型(Java Memory Model,即 JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果。Java 内存模型的主要目标就是 定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。

本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

如何理解 JMM

随着多核 CPU 的发展,CPU 缓存通常分成了三个级别:L1L2L3,级别越小越接近 CPU,所以速度也更快,同时也代表着容量越小。

L1 是最接近 CPU 的, 它容量最小(例如:32K),速度最快,每个核上都有一个 L1 缓存,L1 缓存每个核上其实有两个 L1 缓存, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。L2 缓存 更大一些(例如:256K),速度要慢一些, 一般情况下每个核上都有一个独立的 L2 缓存; L3 缓存是三级缓存中最大的一级(例如 3MB),同时也是最慢的一级, 在同一个 CPU 插槽之间的核共享一个 L3 缓存。

而缓存就会导致内存一致性的问题,为了解决 CPU 本地缓存和主内存数据不一致的问题,引入了缓存一致性协议。

而 JMM 在 JVM 中,就是相当于缓存一致性协议的东西,用来定义数据读写的规则。

image.png

JMM 定义了什么

整个Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性:

  1. JMM 能保证基本的原子性,对于代码块的原子性,可以通过 synchronized 关键字来实现
  2. Java 提供了 volatile 关键字来保证可见性,当共享变量被 volatile 修饰时,它的修改会被立即同步到主存中
  3. volatile 关键字还能保证有序性

对于可见性和有序性,实际上 synchronized 关键字也可以保证。

Happens-Before 规则

我们暂且不讨论三个关键字,因为要说的话太多了,可以单独抽出篇幅来讲了。

A happens-before B就是A先行发生于B(这种说法不是很准确),在Java内存模型中,happens-before的意思是前一个操作的结果可以被后续操作获取。

JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

具体来说,Happens-Before 包含以下规则:

  1. 程序顺序规则:在一个线程内一段代码的执行结果是有序的,哪怕还会发生指令重排,但结果是按照我们代码的顺序生成的,不会改变
  2. 管程锁定规则:无论在单线程还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取这个锁能看到前一个线程的操作结果
  3. volatile 变量规则:如果一个变量被 volatile 修饰,那么线程 A 对这个变量的写操作对于线程 B 来说是可见的
  4. 线程启动规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作
  5. 线程加入规则:Thread 对象的结束先行发生于 join() 方法返回
  6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生
  7. 传递性规则:happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)
  8. 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的 finalize() 方法