文章目录
Java内存模型(JMM)
Java内存模型(JMM)的本质是一个抽象规范,用于定义和描述多线程程序在不同内存区域(如寄存器、CPU缓存、主内存)之间如何交互、共享数据。
JMM 并不是一个真实存在的硬件或软件模块,而是一种逻辑规范,由 Java 虚拟机(JVM)在实现中遵循,以屏蔽底层硬件和操作系统之间的差异,确保多线程程序的正确性和可移植性。
JMM 的本质:
- 抽象的概念模型
JMM 是用来描述线程如何与共享内存交互的一组规则和协议。它并不是一个具体的实体(如代码、工具或模块),而是一种用于指导 JVM 开发者设计和实现内存管理的理论框架。- 规范性 vs 实现性
- 规范性: JMM 定义了内存访问规则,例如变量的可见性、指令的有序性等,这些是程序员需要遵守的原则。
- 实现性: JVM 开发者通过实现 JMM 来确保多线程程序在不同硬件和操作系统上都表现一致。
- 面向硬件和软件的抽象层
JMM 抽象了多线程程序运行时对硬件(如 CPU 缓存、内存)和操作系统(如线程调度、内存模型)的依赖,为开发者提供了一个统一的编程模型。
1. JMM的三大特性
Java内存模型(Java Memory Model, JMM)是一种用于多线程编程的抽象机制,用来规范线程之间对共享变量的访问规则,确保程序的正确性。
它主要解决多线程编程中 原子性、可见性 和 有序性 的问题。
1.1 原子性(Atomicity)
定义: 原子性是指操作不可被中断,要么全部执行,要么完全不执行。
在多线程环境中,某些操作必须以原子方式进行,避免多个线程同时修改同一资源时产生不一致的状态。
例子:
int a = 0;
Thread A: a = 1;
Thread B: a = 2;
结果: 变量 a
的值要么是 1
,要么是 2
,不会出现“中间状态”。但这种原子性在 基本数据类型 的赋值中只适用于 32 位的变量。如果是 64 位的 long
或 double
类型,在 32 位 JVM 中赋值并不具有原子性,可能会分为两个 32 位操作,导致数据的不一致。
**解决方法:**Java 提供了以下原子性保障工具:
- 基本数据类型的读写 是原子的(如
int
、boolean
)。 - 使用
synchronized
关键字保证代码块的原子性。 - 使用
java.util.concurrent.atomic
包中的类(如AtomicInteger
)实现原子性。
1.2 可见性(Visibility)
定义: 可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
在多线程环境中,由于线程可能会对共享变量进行缓存或优化,修改后的值不会立刻更新到主内存,导致其他线程看不到最新的数据。
例子:
boolean flag = false;
Thread A:
while (!flag) {
// busy-wait
}
Thread B:
flag = true;
问题: 在某些 CPU 缓存模型下,线程 A 可能会一直读取到 false
,而看不到线程 B 的更新。
解决方法:
- 使用
volatile
关键字保证可见性。 - 使用 锁(synchronized) 来强制线程之间的内存一致性。
1.3 有序性(Ordering)
定义: 有序性是指程序按照代码的先后顺序执行。
但为了提高性能,编译器和处理器可能会对指令进行重排序(Instruction Reordering),只要不影响单线程环境下的最终结果,JVM 会允许这种优化。
指令重排:
int a = 10;
int b = 20;
a = a + 1;
b = b * 2;
可能会被重排为:
int b = 20;
b = b * 2;
int a = 10;
a = a + 1;
多线程下的问题: 重排可能导致线程之间的可见性和一致性问题。例如,某线程可能在变量尚未完全初始化时就读取它。
解决方法:
volatile
:禁止特定变量的重排序。- 锁(synchronized):保证线程之间的操作顺序。
1.4 Happen-Before 规则
Happen-Before 是 JMM 中定义的一个原则,用来描述两个操作的执行顺序。如果一个操作 A
的结果对操作 B
可见,那么 A
必须 Happen-Before B
。
常见规则:
- 程序顺序规则: 单线程中的代码按顺序执行。
volatile
规则: 对一个volatile
变量的写操作,Happen-Before 后续对该变量的读操作。- 锁规则: 对某个锁的解锁操作,Happen-Before 随后的加锁操作。
- 线程启动规则: 线程的
start()
方法,Happen-Before 线程内的所有操作。 - 线程终止规则: 一个线程的所有操作,Happen-Before 其他线程对该线程调用
join()
返回。 - 中断规则: 对线程的
interrupt()
方法调用,Happen-Before 被中断线程检测到中断事件。 - 对象终结规则: 一个对象的构造完成,Happen-Before 该对象的
finalize()
方法。
1.5 总结
三大特性
- 原子性: 保证操作不可分割。
- 可见性: 保证线程间的共享变量修改可被立即看到。
- 有序性: 保证操作按预期的顺序执行。
在多线程编程中,正确使用 volatile
和 锁 可以有效解决这些问题,同时结合 Happen-Before 规则,确保代码的线程安全性和正确性。
三大特性之间的关系
- 原子性和可见性:
synchronized
同时保障了原子性和可见性。volatile
只保障可见性,不保障原子性。
- 可见性和有序性:
volatile
和 Happen-Before 规则保障了指令的有序性和内存可见性。
- 原子性和有序性:
- 原子操作的语义本身包含一定的有序性,如写入和读取值的顺序。
2. Java线程内存模型结构
Java线程内存模型规定了多线程环境下,线程之间如何共享数据以及如何同步的机制。
主要包括以下几个部分:
这些概念都是Java线程内存模型(Java Memory Model,JMM)中用来保证多线程环境下数据一致性、可见性和顺序性的重要机制。
下面我将一一解释这些概念在多线程环境中的作用和它们的实际意义:
2.1 主内存(Main Memory)
- 作用:主内存是所有线程共享的内存区域,存储着所有线程都能访问的共享变量。
- 多线程环境中的作用:
- 在多线程环境中,多个线程可能会访问和修改这些共享变量。
- 主内存是线程间通信的唯一途径。线程通过读取主内存中的变量或将修改后的值写回主内存,来实现数据共享。
- 问题:由于每个线程有自己的工作内存(工作内存会保存共享变量的副本),不同线程对共享变量的修改可能不会立即对其他线程可见,这会导致数据不一致性的问题。
2.2 工作内存(Working Memory)
- 作用:每个线程都有自己的工作内存,用来存储该线程所使用的共享变量的副本(从主内存复制过来的)。
- 多线程环境中的作用:
- 线程对变量的操作都是在工作内存中进行的。工作内存相当于每个线程对共享变量的“本地缓存”,它只会读取自己工作内存中的副本。
- 对工作内存中共享变量副本的修改,不会立刻反映到其他线程的工作内存中,也不会直接修改主内存中的值,直到同步机制触发。
- 问题:由于每个线程都有自己的工作内存,不同线程之间的工作内存可能包含不同的副本,导致它们对同一共享变量的修改不一致。因此,需要通过一些机制(如内存屏障、volatile关键字、synchronized等)来确保线程间的数据同步和一致性。
2.3 内存屏障(Memory Barriers)
- 作用:内存屏障是通过限制CPU对指令的执行顺序、缓存和优化等行为,来保证多线程操作的有序性和可见性。
- 多线程环境中的作用:
- 读屏障:确保读取操作不会读取到已经被修改但尚未同步的数据。即确保读取操作发生在同步之前。
- 写屏障:确保写操作不会覆盖未同步的数据,确保写操作之前的数据已经被正确更新。
- 问题:多线程中的操作顺序和内存访问顺序是非常重要的,内存屏障通过强制CPU按照一定顺序执行操作,避免优化、重排序等造成的问题,保证线程之间的同步。
2.4 happens-before关系
- 作用:
happens-before
是一个定义操作顺序的关系,用来保证多线程操作的有序性。它是一种偏序关系,用来描述某些操作在多线程环境下必须先于其他操作发生,或者某些操作的结果对其他操作是可见的。 - 多线程环境中的作用:
happens-before
关系用来保证线程之间的数据一致性和正确性。- 如果两个操作之间有
happens-before
关系,意味着前一个操作的结果对后一个操作可见,并且前一个操作在时间上必须发生在后一个操作之前。
- 应用:
- 锁操作:一个线程释放锁,另一个线程获取锁,释放锁的操作
happens-before
获取锁的操作。 - volatile变量:对于一个写操作
happens-before
它之后的读操作。即,如果一个线程写入一个volatile
变量,另一个线程能立刻读取到这个变量的最新值。 - 线程启动:调用
Thread.start()
,保证线程开始执行时,之前的操作happens-before
线程开始执行的操作。
- 锁操作:一个线程释放锁,另一个线程获取锁,释放锁的操作
2.5 这些概念如何协同工作?
- 主内存和工作内存的配合是Java线程内存模型的核心。线程的操作大部分发生在工作内存中,而所有线程共享的变量(即共享数据)存储在主内存中。为了保证多线程程序的正确性,我们需要通过适当的同步机制来确保工作内存与主内存之间的数据一致性。
- 内存屏障提供了一种机制来控制指令的执行顺序,从而确保线程在执行时的操作顺序符合预期。
- happens-before关系确保了在多线程并发情况下,程序操作的执行顺序是确定的,并能保证操作间的正确同步。
这些机制是为了解决在多线程环境下常见的可见性(一个线程修改的数据,另一个线程是否能够看到)、有序性(多个线程操作的执行顺序是否符合预期)和原子性(多线程并发操作时是否会发生冲突)等问题。
2.6 总结:
- 主内存和工作内存是物理内存的组织,控制数据共享和访问。
- 内存屏障则确保数据的顺序和一致性。
- happens-before关系提供了操作之间的同步规则,确保数据在多线程环境中的可见性和有序性。