理解什么是 JMM

本文已收录至 GitHub https://github.com/yifanzheng/java-notes

Java 虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——Java 内存模型。也就是说,Java 内存模型是 Java 虚拟机中定义的一种并发编程的底层模型机制。

JMM 概念

JMM(Java Memory Model)就是 Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的程序运行在不同的系统上会出现各种问题。因此 Java 内存模型屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证一致的并发效果。

JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM 的规定:

  • 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

  • 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量。

  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存中转来完成。

总之,Java 内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

JMM 的抽象示意图:

JMM

从图中可以更直观的看出,每个线程的内存都是独立的,线程对变量的操作只能在工作内存中进行,然后刷回到主存。这便是 Java 内存模型定义的线程基本工作方式。

JMM 的八种内存交互操作

为了更直观,先来看看这张图吧:

JMM 内存交互

  1. lock(锁定):作用于主内存中的变量,一个变量在同一时间只能被一个线程锁定,即把变量标识为线程独占状态。
  2. read(读取):作用于主内存变量,表示把一个变量值从主内存传输到线程的工作内存中,以便下一步的 load 操作使用。
  3. load(载入):作用于线程的工作内存的变量,表示把 read 操作从主内存中读取的变量值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)。
  4. use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作。
  5. assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作。
  6. store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便下一步的 write 操作使用。
  7. write(写入):作用于主内存的变量,表示把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
  8. unlock(解锁):作用于主内存的变量,表示把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM 还规定了以上八种操作需按照如下规则进行:

  • 不允许read 和 load、store 和 write 操作之一单独出现,也就是 read 操作后必须 load,store 操作后必须 write。即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许线程丢弃它最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许线程将没有 assign 的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。也就是对变量实施 use 和 store 操作之前,必须经过 load 和 assign 操作。
  • 一个变量同一时间只能有一个线程对其进行 lock 操作。但 lock 操作可以被同一条线程重复执行多次,多次 lock 之后,必须执行相同次数 unlock 才可以解锁。
  • 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值。
  • 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量。
  • 一个线程对一个变量进行 unlock 操作之前,必须先把此变量同步回主内存。

JMM 三大特征

JMM 三大特征分别是:原子性,可见性,有序性。整个 JMM 实际上也是围绕着这三个特征建立起来的,并且也是 Java 并发编程的基础。

原子性

原子性是指一个操作是不可分割、不可中断的,要么全部执行成功要么全部执行失败。

JMM 只能保证对基本数据类型的变量的读写操作是原子性的,但 long 和 double 除外(long 和 double 的非原子性协定)。

我们来看看下面的例子:

int x = 1;
int y = x;
x ++;

上面三行代码只有第一行是原子性操作,基本类型赋值操作,必定是原子性操作。

第二行代码先读取 x 变量的值,再进行赋值给 y 变量,进行了两个操作,不能保证原子性。

第三行代码先读取 x 变量的值,再进行加 1,最后再赋值给 x 变量,进行了三个操作,不能保证原子性。

在并发环境下,为了保证原子性,Java 提供了 synchronized 关键字。因此在 synchronized 修饰的代码块之间的操作都是原子性的。

可见性

可见性是指所有线程都能看到共享内存的最新状态。即当一个线程修改了一个共享变量的值时,其他线程能够立即看到该变量的最新值。

对于可见性问题,Java 是提供了一个 volatile 关键字来保证可见性。当一个共享变量被 volatile 关键字修饰时,这个变量被修改后会立即刷新到主内存,保证其他线程看到的值一定是最新的。

除了 volatile 关键字之外,final 和 synchronized 也能实现可见性。

final 关键字修饰的变量,在构造器中一旦初始化完成,如果没有对象逸出(指对象没有初始化完成就可以被别的线程使用),那么其他线程都就可以看见 final 修饰的变量。

synchronized 的原理是,线程进入 synchronized 代码块后,线程会获取到 lock,将会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程执行 unlock。

有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。

在 Java 中,可以通过 volatile 和 synchronized 关键字来保证多线程之间操作的有序性。

volatile 关键字是通过在主存中加入内存屏障来达到禁止指令重排序,来保证有序性。

synchronized 关键字原理是,一个变量在同一时刻只能被一个线程 lock,并且必须 unlock 后,其他线程才可以重新 lock,使得被 synchronized 修饰的代码块在多线程之间是串行执行的。

参考

[1] Java 内存模型(JMM)总结: https://zhuanlan.zhihu.com/p/29881777

[2] Java 内存模型(JMM)详解: https://juejin.im/post/6844903986663800845

[3] 再有人问你 Java 内存模型是什么,就把这篇文章发给他。: https://www.hollischuang.com/archives/2550

[4] Java Memory Model: http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

因为是个人学习笔记,难免存在一些错误或纰漏,也请小伙伴们指正。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐