JVM_JMM
一、JMM 是什么?
核心定义
JMM 是一套规范,它定义了Java程序中各种变量(线程共享变量)的访问方式,以及在并发环境下如何保证数据的可见性、有序性和原子性。
关键点:
它不是物理内存模型:JMM并不真实存在,它只是一个抽象的规则和约定,描述的是线程和主内存之间的抽象关系。
它是一种规范:它规定了JVM如何与计算机内存协同工作,特别是在多线程环境下。
它的目标:屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到一致的内存访问效果,从而保证并发程序的正确性。
你可以把它想象成 Java并发世界的“宪法”,它规定了线程之间如何通信、如何共享数据,所有JVM实现(如HotSpot、J9等)都必须遵守这套规范。
二、为什么需要 JMM?—— 解决的问题
计算机硬件(CPU、内存)为了提升性能,做出了一些优化,但这些优化会带来一些问题:
CPU高速缓存 vs. 可见性问题
现象:现代CPU都有多级缓存(L1, L2, L3)。线程运行时,会将主内存中的数据拷贝一份到自己的工作内存(CPU缓存)中操作。操作完成后,再写回主内存。
问题:如果一个线程修改了自己工作内存中的副本,但没有及时写回主内存,或者其他线程没有及时从主内存刷新最新值,就会导致其它线程看不到最新的修改。这就是可见性问题。
指令重排序 vs. 有序性问题
现象:为了充分利用CPU资源,编译器和处理器可能会对指令的执行顺序进行重新排序(在不改变单线程程序执行结果的前提下)。
问题:在多线程环境下,这种重排序可能会破坏程序的逻辑,导致结果出乎意料。这就是有序性问题。
JMM就是为了解决这些由硬件优化带来的可见性和有序性问题而存在的。
它通过定义一些关键的关键字(如 volatile
、synchronized
)和 Happens-Before
规则,为开发者提供了一套可靠的多线程内存访问机制。
三、JMM 的核心结构:主内存与工作内存
JMM从抽象角度将内存划分为两类:
内存 | 描述 | 对应物理实体 |
---|---|---|
主内存 (Main Memory) | 存储所有共享变量(实例字段、静态字段等)。所有线程都能访问。 | 物理内存(RAM)的一部分 |
工作内存 (Working Memory) | 每个线程私有的内存空间。存储该线程使用到的变量的主内存副本。线程对变量的所有操作(读、写)都必须在工作内存中进行,不能直接读写主内存。 | CPU寄存器、高速缓存(L1, L2 Cache) |
交互流程(简化版):
Read(读取):线程从主内存读取共享变量到自己的工作内存。
Load(加载):将read操作得到的值放入工作内存的变量副本中。
Use(使用):线程执行引擎使用工作内存中的变量值。
Assign(赋值):线程将一个新值赋给工作内存中的变量副本。
Store(存储):将工作内存中改变后的变量值传递到主内存。
Write(写入):将store操作带来的值放入主内存的变量中。
这个过程图示如下:
正是因为这个复杂的交互过程,如果缺乏同步,就会导致数据不一致。
四、JMM 解决的三大问题
JMM 主要围绕解决以下三个核心问题来设计:
1. 原子性 (Atomicity)
问题:一个或多个操作要么全部执行成功,要么全部不执行,中间不能被中断。
JMM保障:JMM直接保证了基本数据类型的访问、读写是原子性的(
long
和double
非原子性协定,但商用JVM都实现了其原子性)。对于更大范围的代码块,可以通过synchronized
关键字或Lock
接口来保证其原子性。
2. 可见性 (Visibility)
问题:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
JMM保障:
volatile
关键字:保证修改的值能立即被更新到主内存,并使其他线程中该变量的缓存行失效,从而强制其他线程必须从主内存重新读取最新值。synchronized
关键字:在解锁前,必须将工作内存中的变量同步到主内存(执行store-write
)。final
关键字:被final
修饰的字段在构造完成后就是可见的(前提是没有this
引用逸出)。
3. 有序性 (Ordering)
问题:程序执行的顺序按照代码的先后顺序执行。禁止指令重排序。
JMM保障:
volatile
关键字:通过插入内存屏障(Memory Barrier)来禁止指令重排序。synchronized
关键字:规定一个变量在同一个时刻只允许一条线程对其进行lock
操作,这使得持有同一个锁的两个同步块只能串行进入,从而保证了有序性。Happens-Before 原则:JMM最核心的规则,天然保证了有序性。
五、Happens-Before 原则
这是JMM的精髓。它定义了哪些操作在内存层面是“可见”的,即前一个操作的结果对后续操作是可见的。无需任何同步手段,如果操作A Happens-Before 操作B,那么A所做的任何操作对B都是可见的。
常见规则:
程序次序规则:在一个线程内,书写在前面的操作先行发生于后面的操作。
管程锁定规则:一个
unlock
操作先行发生于后面对同一个锁的lock
操作。volatile
变量规则:对一个volatile
变量的写操作先行发生于后面对这个变量的读操作。线程启动规则:
Thread
对象的start()
方法先行发生于此线程的每一个动作。线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测(如
Thread.join()
)。线程中断规则:对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生。对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()
方法的开始。传递性:如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
总结
方面 | 解释 |
---|---|
JMM是什么 | Java内存模型,一套规范,定义了多线程下如何访问共享变量。 |
目的 | 解决由于硬件优化(缓存、重排序)导致的可见性和有序性问题,提供跨平台的内存访问一致性。 |
核心结构 | 主内存(共享)和工作内存(线程私有)。 |
关键保障 | 原子性(synchronized )、可见性(volatile , synchronized )、有序性(volatile , synchronized , Happens-Before)。 |
重要性 | 不理解JMM,就无法真正理解 synchronized 、volatile 和 java.util.concurrent 包的工作原理。 |
简单来说,JMM是Java并发编程的基石,它定义了线程如何与内存交互的规则,确保了多线程程序在不同平台上的正确性和可预测性。