Java内存模型(JMM)速览
目录
注意
本文最后更新于 2024-05-06,文中内容可能已过时。
Java内存模型(JMM)是Java并发编程的“宪法”,它不是物理层面的内存划分,而是一组抽象的规则和规范,用于解决多线程环境下共享变量的访问一致性问题。
一、JMM的核心目标
屏蔽不同硬件(CPU缓存架构)和操作系统对内存访问的差异,保证Java程序在所有平台上都能获得一致的内存访问效果。
二、JMM的三大核心问题
并发编程的三大核心问题,本质上都是由于JMM中“主内存-工作内存”的交互机制导致的。
- 可见性(Visibility)
- 问题:当一个线程修改了共享变量的值,其他线程不能立即感知到该修改。
- 原因:线程将变量缓存在自己的工作内存中,修改后未立即同步回主内存。
- 解决方案:
volatile、synchronized、final。
- 原子性(Atomicity)
- 问题:一个或多个操作在执行过程中被中断,导致执行结果不符合预期。
- 经典案例:
i++不是原子操作,它包含“读取→计算→写回”三个步骤。 - 解决方案:
synchronized、Lock、AtomicInteger等原子类。
- 有序性(Ordering)
- 问题:为了优化性能,编译器和处理器会对指令进行重排序,导致多线程下的执行顺序与代码顺序不一致。
- 解决方案:
volatile(通过内存屏障禁止重排序)、synchronized(保证同一时刻只有一个线程执行)。
三、JMM的内存抽象:主内存与工作内存
JMM将所有变量存储在主内存中,每个线程拥有自己的工作内存。
- 主内存(Main Memory)
- 所有线程共享的内存区域,存储所有实例变量、静态变量等共享变量。
- 物理上大致对应于RAM(随机存取存储器)。
- 工作内存(Working Memory)
- 每个线程私有的内存区域,存储该线程使用的共享变量的副本。
- 物理上大致对应于CPU的寄存器、L1/L2缓存。
- 线程对变量的所有操作(读、写)都必须在工作内存中进行,不能直接读写主内存。
- 线程间通信机制
- 变量值的传递必须通过主内存完成:线程A修改变量→刷新到主内存→线程B从主内存读取→加载到自己的工作内存。
- 八种原子操作 JMM定义了8种原子操作来实现主内存和工作内存的交互:
read(读取):从主内存传输变量到工作内存。load(载入):把read得到的值放入工作内存的变量副本中。use(使用):把工作内存中的变量值传递给执行引擎。assign(赋值):将执行引擎接收到的值赋给工作内存变量。store(存储):把工作内存中的变量值传送到主内存。write(写入):把store得到的值放入主内存的变量中。lock(锁定):标识主内存中的变量为线程独占。unlock(解锁):释放主内存中变量的锁定状态。
四、JMM的核心规则:Happens-Before原则
Happens-Before是JMM判断数据是否存在竞争、线程是否安全的核心规则。如果操作A Happens-Before操作B,那么A的执行结果对B是可见的,且A的执行顺序排在B之前。
- 程序顺序规则
- 同一个线程中,按照代码顺序,前面的操作happens-before后面的操作。
- 监视器锁规则
- 对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则
- 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 线程启动规则
Thread.start()的调用,happens-before于该线程中的任何操作。
- 线程终止规则
- 线程中的所有操作,happens-before于其他线程检测到该线程已经终止(如
join()返回)。
- 中断规则
- 对线程
interrupt()的调用,happens-before于被中断线程检测到中断事件。
- 传递性
- 如果A Happens-Before B,且B Happens-Before C,则A Happens-Before C。
五、JMM的实现机制
- volatile关键字
- 保证可见性:写操作立即刷新到主内存,读操作立即从主内存加载。
- 禁止重排序:通过插入内存屏障(Memory Barrier)禁止编译器和处理器对volatile变量的读写操作进行重排序。
- 不保证原子性:复合操作(如
v++)仍需配合锁使用。
- synchronized关键字
- 保证原子性:通过独占锁保证同一时刻只有一个线程执行临界区代码。
- 保证可见性:解锁前必须将工作内存中的变量同步回主内存,加锁时必须从主内存重新加载变量。
- 保证有序性:通过“同一时刻只有一个线程执行”保证有序性。
- final关键字
- 构造完成后,final字段对其他线程可见,且不可修改。
六、JMM与JVM内存模型的区别
| 对比维度 | JMM | JVM内存模型 |
|---|---|---|
| 核心范畴 | 并发编程的理论基石 | JVM的运行时内存结构 |
| 主要目标 | 解决多线程环境下的可见性、原子性、有序性问题 | 定义程序运行时数据的存储、分配和管理 |
| 抽象概念 | 主内存、工作内存 | 堆、虚拟机栈、方法区等 |
| 具体实现 | volatile、synchronized、final等关键字的语义 | 具体的内存区域划分,如堆内存、栈帧等 |
| 关注点 | 线程如何与内存交互,保证线程安全 | 内存如何分配、回收,以及对象的生命周期 |
七、案例:DCL单例模式为何需要volatile
双重检查锁(DCL)单例模式必须加volatile,否则可能导致其他线程获取到未初始化完成的实例。
|
|