JMM(Java 内存模型)详解
JMM(Java 内存模型)主要定义了共享变量在多线程环境中的可见性和操作顺序。在多线程环境下,线程如何访问主内存中的共享数据,如何保证数据的同步性,是并发编程中的核心问题。
从 CPU 缓存模型说起
为什么要弄一个 CPU 高速缓存?
在程序运行过程中,CPU 缓存的引入是为了提升程序的执行效率。和我们常见的缓存(例如 Redis)类似,CPU 高速缓存是为了解决 CPU 处理速度和内存处理速度之间的不匹配问题。内存的访问速度远远低于 CPU,所以将常用数据放入 CPU 缓存,以提高读取速度。
CPU 缓存的工作方式
现代 CPU 通常具有多层次缓存(L1、L2、L3),当 CPU 需要数据时,首先从缓存中获取。如果缓存中没有,再从内存中读取。这样可以避免 CPU 频繁访问较慢的主内存,从而提高执行效率。
但是,多个线程同时对同一数据进行操作时,可能导致缓存不一致性问题。例如,两个线程读取缓存中的同一个变量副本,分别进行修改后再写回主内存,这样会导致最终的结果与预期不一致。为了保证缓存一致性,CPU 引入了缓存一致性协议(如 MESI 协议)。
操作系统与内存一致性
操作系统在虚拟化硬件资源时,也要处理内存一致性问题。每个操作系统都有自己定义的内存模型,用来确保多核处理器和内存之间的数据一致性。
指令重排序
什么是指令重排序?
为了提高程序执行效率,计算机在执行程序时,往往会对指令进行重排序。这种重排序主要包括:
- 编译器优化重排:编译器根据优化策略,在不改变单线程程序语义的前提下,调整代码的执行顺序。
- 指令并行重排:现代处理器采用指令级并行技术(ILP),如果指令之间没有数据依赖性,处理器可以将这些指令进行重排执行。
指令重排通常不会影响单线程程序的正确性,但在多线程环境下,可能会导致不同线程间的操作顺序不一致,从而引发并发问题。
JMM(Java 内存模型)
什么是 JMM?
JMM(Java Memory Model)是 Java 为了简化多线程编程、增强程序的可移植性而定义的一套规范。它规范了线程和主内存之间的关系,确保多线程并发操作时的可见性和顺序性。
JMM 抽象了线程和主内存之间的交互方式,规定了一个线程对共享变量的修改如何对其他线程可见,并确保在并发环境下程序的执行顺序符合预期。
线程与内存的交互
在 JMM 中,线程的操作不直接对主内存进行读写,而是将数据存储在本地内存(如 CPU 寄存器或缓存)中。每个线程在本地内存中有一个共享变量的副本。线程对共享变量的修改,需要通过以下步骤同步到主内存:
- 线程对本地内存的共享变量进行修改。
- 线程将修改后的数据同步回主内存。
- 其他线程从主内存读取共享变量的最新值。
主内存与本地内存
- 主内存:所有线程的共享变量都存放在主内存中。实例对象、类信息、常量和静态变量也存储在主内存中。
- 本地内存:每个线程有自己的本地内存,存储该线程在本地修改的共享变量副本。线程只能操作自己的本地内存,而不能直接操作其他线程的本地内存。
JMM 的同步操作
JMM 定义了一些同步操作,以保证线程间的数据一致性和顺序性。以下是 JMM 提供的常见同步操作:
- 锁定(lock):将主内存中的变量锁定,使该变量成为一个线程独享的资源。
- 解锁(unlock):解除主内存中对变量的锁定状态,允许其他线程访问。
- 读取(read):将主内存中的数据读取到线程的本地内存中。
- 写入(write):将线程本地内存中的数据写回主内存。
- 赋值(assign):将值从执行引擎传给线程本地内存中的变量。
happens-before 原则
什么是 happens-before?
happens-before 是 JMM 中的一个关键概念,用于定义操作间的内存可见性和顺序。它规定了两个操作之间的执行顺序和可见性:如果操作 A happens-before 操作 B,那么操作 A 的结果对操作 B 是可见的,并且 A 必须在 B 之前执行。
happens-before 主要用于解决并发程序中由于指令重排或缓存不一致性带来的问题,确保操作的顺序和内存的可见性。
happens-before 规则
- 程序顺序规则:一个线程中的操作按照代码顺序执行,即前面的操作 happens-before 后面的操作。
- 解锁规则:对一个变量的解锁操作 happens-before 对该变量的加锁操作。
- volatile 变量规则:对 volatile 变量的写操作 happens-before 后续的读操作。
- 传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
- 线程启动规则:调用
Thread.start()
方法 happens-before 线程的任何动作。
举例
int a = 1; // 操作 1
int b = 2; // 操作 2
int c = a + b; // 操作 3
- 操作 1 happens-before 操作 2。
- 操作 2 happens-before 操作 3。
- 操作 1 happens-before 操作 3。
虽然可以对这三条操作进行重排序,但必须遵循 happens-before 关系来保证操作的顺序和可见性。
JMM 与并发编程
原子性
原子性保证了操作要么完全执行,要么完全不执行,不会中途被打断。在 Java 中,原子性操作可以通过 synchronized
、Lock
或 volatile
等机制来实现。
可见性
可见性指的是一个线程对共享变量的修改,能够被其他线程及时看到。Java 提供了 volatile
关键字来保证可见性,volatile
变量的修改对所有线程立即可见。
有序性
有序性保证了程序中操作的顺序与程序代码的顺序一致。在多线程环境下,由于指令重排,可能会导致程序的执行顺序不符合预期。通过 synchronized
、volatile
和内存屏障,可以保证操作的有序性。
总结
- JMM 是 Java 用来确保多线程程序正确性的内存模型,主要负责线程间共享数据的可见性和顺序性。
- JMM 抽象了线程和主内存之间的交互,确保线程间共享数据的正确同步。
- JMM 提供了 happens-before 原则来约束操作之间的顺序和可见性,防止多线程下的指令重排引发的问题。
JMM 的设计目标是简化多线程编程,使程序员不必关心底层硬件的细节,直接通过合适的并发工具(如 volatile
、synchronized
、Lock
等)来开发出安全的并发程序。