1. 定义
Java 内存模型(Java Memory Model,JMM)是一种抽象的概念,并不真实存在,它是 Java 虚拟机规范中定义的一组规则,用于屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果,保证多线程环境下程序的正确性。
2. 主要目的
在多线程环境下,解决以下几个问题:
- 可见性问题:当一个线程修改了共享变量的值,其他线程能够及时看到这个修改。
- 原子性问题:保证对共享变量的某些操作是不可分割的,要么全部执行,要么都不执行。
- 有序性问题:程序执行的顺序按照代码的先后顺序执行,避免指令重排序带来的问题。
3. 内存结构
JMM 规定了 Java 程序中变量(包括实例字段、静态字段和数组元素)的访问规则,将内存划分为主内存和工作内存:
- 主内存(Main Memory)
- 主内存是所有线程共享的内存区域,它存储了 Java 对象的实例和类的静态变量等。所有的变量都存储在主内存中。
- 线程不能直接操作主内存中的变量,对变量的读写操作都需要通过工作内存来完成。
- 工作内存(Working Memory)
- 每个线程都有自己独立的工作内存,它是主内存的一个副本拷贝。线程在工作内存中对变量进行操作,操作完成后再将结果同步到主内存中。
- 工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
4. 操作规则
JMM 定义了以下 8 种原子操作来实现主内存和工作内存之间的交互:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 操作使用。
- load(载入):作用于工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
- write(写入):作用于主内存的变量,把 store 操作从工作内存中得到的变量值放入主内存的变量中。
5. 三大特性
5.1 原子性
- 含义:一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 示例:基本数据类型的读取和赋值操作是原子性的,例如
int a = 1;
是原子操作;但像i++
这样的操作不是原子性的,它实际上包含了读取、加 1、写入三个操作。 - 保证方式:可以使用
synchronized
关键字或者Lock
接口来保证操作的原子性。
5.2 可见性
- 含义:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 示例:在多线程环境下,如果一个线程修改了一个共享变量的值,而其他线程没有及时看到这个修改,就会出现可见性问题。
- 保证方式:
- 使用
volatile
关键字,volatile
修饰的变量会保证对它的写操作会立即刷新到主内存中,读操作会直接从主内存中读取。 - 使用
synchronized
关键字,在同步块中对变量的修改会在释放锁时刷新到主内存中,其他线程在获取锁时会从主内存中读取最新的值。 - 使用
Lock
接口,和synchronized
类似,在释放锁时会保证变量的可见性。
- 使用
5.3 有序性
- 含义:程序执行的顺序按照代码的先后顺序执行。但在实际执行过程中,为了提高性能,编译器和处理器可能会对指令进行重排序。
- 示例:
int a = 1; // 语句 1
int b = 2; // 语句 2
int c = a + b; // 语句 3
在没有数据依赖的情况下,语句 1 和语句 2 可能会被重排序,但语句 3 不会在语句 1 和语句 2 之前执行,因为存在数据依赖。
- 保证方式:
- 使用
volatile
关键字,volatile
关键字可以禁止指令重排序。 - 使用
synchronized
关键字和Lock
接口,它们可以保证在同步块内的代码是有序执行的。
- 使用
6. Happens-Before 原则
Happens-Before 原则是 JMM 中定义的一些规则,用于判断一个操作是否对另一个操作可见。如果一个操作 Happens-Before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。主要的 Happens-Before 规则如下:
- 程序顺序规则:一个线程中的每个操作,Happens-Before 于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,Happens-Before 于随后对这个锁的加锁。
- volatile 变量规则:对一个
volatile
域的写,Happens-Before 于任意后续对这个volatile
域的读。 - 传递性:如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
- 线程启动规则:
Thread
对象的start()
方法 Happens-Before 于此线程的每一个动作。 - 线程终止规则:线程中的所有操作都 Happens-Before 于对此线程的终止检测。
- 线程中断规则:对线程
interrupt()
方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生。 - 对象终结规则:一个对象的初始化完成(构造函数执行结束)Happens-Before 于它的
finalize()
方法的开始。