首先,当问到 Java 内存模型的时候,一定要注意,这块不要和 JVM 内存布局(JVM 运行时数据区域)搞混了,这块问的是 Java 内存模型,Java Memory Model,简称 JMM,而不是 JVM 的内存布局。

Java 内存模型是用来定义 Java 线程和内存之间的操作规范的,目的是解决多线程正确执行的问题。Java 内存模型规范的定义确保了多线程程序的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。 Java 内存模型主要包括以下内容:

  1. 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
  2. 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本。线程只能直接操作工作内存中的数据。
  3. 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
  4. 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
  5. 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
  6. 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。

Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。

内存模型的简单执行示例图如下:

image.png

为什么要有Java内存模型?

只所以要有 Java 内存模型的原因有两个:

  1. CPU 缓存和主内存数据一致性的问题
  2. 操作系统优化指令重排序的问题

我们分别来看下这两个问题。

缓存一致性问题

要讲明白缓存一致性问题,要从计算机的内存结构说起,它的结构是这样的:

image.png

所以从上面可以看出计算机的重要组成部分包含以下内容:

  1. CPU
  2. CPU 寄存器:也叫 L1 缓存,一级缓存。
  3. CPU 高速缓存:也叫 L2 缓存,二级缓存。
  4. (主)内存

当然,部分高端机器还有 L3 三级缓存。

由于主内存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存(L2)来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主内存中,这样就会导致多个线程在进行操作和同步时,导致 CPU 缓存和主内存数据不一致的问题。

操作系统优化和指令重排序问题

并且,由于 JIT(Just In Time,即时编译)技术的存在,它可能会对代码进行优化,比如将原本执行顺序为 a -> b -> c 的流程,“优化”成 a -> c -> b 了,但这样优化之后,可能会导致我们的程序在某些场景执行出错,比如单例模式双重效验锁的场景,这就是典型的好心办坏事的事例。

结论

所以,为了防止缓存一致性问题和操作系统指令重排序导致的问题,于是就有了 Java 内存模型,来规范和定义多线程的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。

Java 内存模型定义了很多东西,比如以下这些:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

就是咱们文章刚开始画的那附图。

主内存和工作内存交互规范

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现(以下内容只需要简单了解即可):

  • lock:锁定,作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock:解锁,作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read:读取,作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • load:载入,作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:使用,作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign:赋值,作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:存储,作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write:写入,作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

注意:工作内存也就是本地内存的意思。

小结

Java 内存模型(Java Memory Model,JMM)是一种规范,定义了 Java 程序中多线程环境下内存访问和操作的规则和语义,主要是解决 CPU 缓存一致性问题和操作系统优化指令重排序的问题的。

参考 & 鸣谢

公众号《爱笑的架构师》