Java Memory Model(JMM: Java内存模型)提到 Java Virtual Machine(JVM: Java虚拟机) 如何与计算机内存工作。JVM是整个计算机模型,所以它包含JMM。
如果你想要设计出正确的并发程序,那么理解JMM是非常重要的。Java内存模型会提到一个线程怎样获取被别的线程修改后的共享变量的值;也会提到当有必要的时候,怎样让多个线程顺序的访问共享变量。
注意:原来的Java内存模型进行过修订,从Java 1.5 到 Java 14+,这个版本的内存模型仍然适用。
1. Java Memory Model
在JVM中,JMM包含两种内存模型:Thread Stacks(线程栈)、Heap(堆内存)。如图所示

内存模型
- 每个在JVM中运行的线程都有自己的线程栈。这个线程栈包含线程中调用的方法信息和程序计数器(当前程序所执行的字节码的行号指示器)。
- 在开始执行某个方法的时候,会在线程栈中存储方法内所有的local variable(局部变量),局部变量只对创建它的线程可见,其他线程无法访问。即使两个线程同时执行同一个方法,这两个线程都会创建局部变量保存至属于自己的线程栈中,线程只能访问自己线程栈中的数据。
- 所有基本数据类型(byte、short、int、long、float、double、char、boolean)的局部变量都保存在线程栈中,并且对其他线程不可见。一个线程可能拷贝一个基本数据类型变量给其他线程,但不会共享原始的局部变量(这里是值传递)。
- 堆内存中包含Java应用程序中所有对象的创建,不论哪个线程创建的对象都存储在堆内存中,并且包含基础数据类型的包装类也存储于堆内存中。对象的创建不管是作为局部变量、还是作为对象的成员,都始终保存于堆内存中。
如下图所示:

- 一个本地变量可能是一个基本数据类型,这种情况下本地变量将完全保存在线程栈中。
- 一个本地变量可能引用一个对象,这种情况下对象的引用保存在线程栈中,对象保存在堆内存中。
- 一个对象可能包含一些方法,方法包含一些本地变量。本地变量保存在线程栈中,方法保存随对象保存在堆内存中。
- 一个对象的成员变量随对象保存在堆内存中,不论这个成员变量是基本数据类型还是引用其他对象,成员变量都保存在堆内存中。
- 静态变量随 class(类) 保存在堆内存中。
堆内存的对象可以被任何具有该对象引用的线程访问。
2. 计算机硬件内存结构
现代硬件内存结构与Java内存模型有些不同,为了更好的理解Java内存模型,了解硬件内存结果是很重要的。
通常的硬件内存结构。如下图

现代计算机通常拥有2个以上的CPU,有些CPU有多个内核。重点是多CPU计算机可以同时执行多个线程。当你的Java应用程序是多线程应用程序,在你的应用程序中可能同时出现每个CPU都在执行线程的情况。
- 每个CPU有一组(注意是一组,多个)CPU寄存器,CPU操作寄存器的速度比操作主内存更快,这也意味着CPU访问寄存器的速度比访问内存更快。
- 每个CPU也可能有一个CPU缓存(CPU Cache Memory),事实上,每个CPU可能有一定数量的CPU缓存。CPU访问CPU缓存的速度比访问内存的速度快,通常情况下CPU访问CPU缓存的速度没有访问寄存器的速度快。因此你可以认为CPU访问内存的顺序依次是:寄存器 > CPU缓存 > 主内存。另外,一些CPU可能会有多级CPU缓存(L1,L2,L3等)。
- 每个计算机都包含一个主内存,主内存的容量比CPU缓存的容量更大。
- 通常情况下,CPU访问内存会读取主内存中的一些数据拷贝至CPU缓存,甚至拷贝至CPU内部的寄存器,然后可以在CPU缓存或者寄存器操作数据。当CPU需要写回数据至主内存中时,它首先会将寄存器中的结果值提交至CPU缓存中,然后再将CPU缓存的值提交至主内存中。
- 当CPU需要将CPU缓存的数据提交至主内存中时,CPU缓存的数据将会被写回主内存中。
2.1 桥接Java内存模型和计算机硬件内存结构
如您所知,Java内存模型与现代计算机硬件内存结构不同,硬件内存结构没有区分线程栈内存和堆内存。也就是说:线程栈内存和堆内存可以都存在于硬件的主内存、CPU高速缓存、寄存器中。

当Java对象和变量可以保存在不同的计算机内存区域中,这就可能会导致一些问题的发生。主要的两个问题是:
- 当线程更新共享数据时,共享数据在多个线程中的可见性。
- 当多个线程读、写、检查共享数据时,产生竞争条件。
下面我们来解释这2个问题
2.2. 共享数据的可见性
如果多个线程共享一个对象,并且代码中没有使用 volatile 关键字和 synchronized 同步代码块。当一个线程更新这个共享对象后,修改后的对象的值可能对其他线程不可见。
想象一下,当一个共享对象保存在主内存中时,一个CPU正在执行某个线程时把主内存中共享对象缓存至CPU高速缓存上,另外一个CPU也把共享对象缓存到CPU高速缓存中,其中一个CPU在CPU高速缓存中更新了共享数据并把共享数据写回到主内存中,此时另外一个CPU的CPU高速缓存还是旧的共享资源数据。
下图中解释了上面这种场景。下图左边的正在执行某个线程的CPU将 obj.count 加载到CPU高速缓存中,同时将 obj.count 的值改为 2。左边这个线程对 obj.count 的值进行修改后,对执行其他线程的CPU并不可见。

解决这个问题我们可以使用 Java 提供的 volatile 关键字。这个关键字用于修饰某个成员变量,当线程要读取这个变量的值时,会从读取主内存中读取变量的值,并且每次修改变量的值时都会重新写回到主内存中。(后面会有文章详细介绍 volatile 关键字)
竞争条件
如果多个线程共享一个对象,并且有多个线程修改共享变量,可能产生竞争条件。
想象一下,如果一个线程A从主内存中读取共享变量 obj.count 并写入CPU高速缓存中,同时线程B也从主内存中读取共享变量 obj.count 并写入CPU高速缓存中,线程A和线程B同时对 obj.count 做自增操作,也就是说 obj.count 同时被自增2次,在每个CPU里面自增一次。如果变量 obj.count 的同步执行递增2次,那么这个变量写回主内存时值应该是 obj.count + 2,然而这2次递增没有同步执行,实际上是在线程A和线程B并行执行,最终线程A和线程B把 obj.count 的值写回数据到主内存后,实际值变成了 obj.count + 1。如下图

解决这个问题我们可以使用 Java 提供的 synchronized 锁。synchronized 可以分别用于 代码块、普通方法、静态方法。后面有文章介绍 synchronized,这里不做过多描述。