java内存模型
概述
现代计算机内存架构是一个复杂的多层次系统,旨在平衡速度、容量和成本。其核心特征是 内存层次结构(Memory Hierarchy)和虚拟内存(Virtual Memory) 技术。

计算机内存被组织成一个金字塔形的层次结构,层级越高,速度越快、成本越高、容量越小;层级越低,速度越慢、成本越低、容量越大。
- 寄存器 (Registers): 位于CPU内部,速度最快,容量最小。它们用于存储CPU当前正在执行的指令和数据,供CPU直接访问和修改。
- 高速缓存 (Cache Memory): 通常分为L1、L2、L3多级缓存,集成在或靠近CPU核心。它存储从主内存中频繁使用的数据和指令的副本,以减少访问主内存的延迟。L1缓存最小最快,L3最大最慢。高速缓存使用快速但昂贵的SRAM(静态随机存取存储器)实现。
- 主内存 (Main Memory / RAM): 即我们通常所说的内存条,使用DRAM(动态随机存取存储器)实现。它的容量远大于缓存,但速度比缓存慢。操作系统和当前运行的程序及数据都驻留在主内存中。
现代计算机分层内存架构虽然提高了内存访问性能,但同时也引入了新的挑战:
- 缓存一致性协议 (Cache Coherence Protocols): 在多核系统中,不同核心可能有同一个共享数据的本地缓存副本。硬件通过缓存一致性协议(如MESI协议)来确保数据在所有缓存中的一致性。
- 指令重排序 (Instruction Reordering): 编译器和CPU为了优化执行效率,可能会改变指令的执行顺序,只要不影响单线程下的结果。
- 写缓冲区 (Write Buffers): CPU将数据写入缓存或主内存时,可能不会立即完成,而是先写入一个写缓冲区,然后继续执行后续指令。
这些优化虽然提升了性能,但在多线程环境下可能导致数据不一致或不可见的问题,这也是Java内存模型等高级语言规范需要解决的核心问题。
java内存模型
Java内存模型(Java Memory Model, JMM) 是一个抽象概念模型,它定义了在多线程环境中,一个线程对共享变量的写入何时对其他线程可见,以及如何保证操作的原子性、可见性和有序性,其核心思想是为了屏蔽底层硬件和操作系统的内存访问差异,确保Java并发程序在各种平台上都能保持一致的行为。在Java内存模型中,它有如下规范:
- 所有的变量都存储在主内存(
Main Memory)中; - 线程都有一个私有的本地内存(
Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本; - 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存;
- 不同的线程之间无法直接访问对方本地内存中的变量。

在上面模型中主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成,Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

从上图中,如果要把一个变量从内存中复制到工作内存中,就需要顺序的执行 read 和 load 操作,如果把变量从工作内存同步到主内存中,就需要执行 store 和 write 操作。 Java 内存模型只定义了上述操作必须按顺序执行,却没要求是连续执行,如果多个线程同时读取修改同一个共享变量,这种情况可能会导致每个线程中的本地内存中缓存变量一致的问题。
在并发场景下,java内存模型其实就是围绕着原子性、可见性和有序性三个特性展开:
- 原子性(Atomicity):确保指令是不可分割的。例如,对非long和double类型变量的读取和写入操作是原子性的。
- 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。由于工作内存的缓存机制,这一点默认是无法保证的。
- 有序性(Ordering):编译器和处理器为了优化性能,可能会对指令进行重排序。JMM需要确保在不影响单线程程序正确性的前提下,多线程程序的执行顺序也符合预期。
Happens-Before 原则(核心规则)
为了解决可见性和有序性问题,JMM引入了Happens-Before关系,它定义了多线程环境中操作之间的顺序关系,是判断数据竞争和线程安全的主要依据。如果操作 A Happens-Before 操作 B (记作 A hb B), JMM 会保证:
- 有序性: 无论编译器和处理器如何重排序,操作 A 的执行结果在时间上看起来总是在操作 B 之前。
- 可见性: 操作 A 的所有内存写入效果对操作 B 必须是可见的。
如果两个操作之间不存在 Happens-Before 关系,那么JVM可以对它们进行任意重排序,其内存可见性也无法保证。以下是 JMM 规范定义的详细 Happens-Before 规则:
程序次序规则 (Program Order Rule):这是最直观的规则,适用于单线程环境。在一个线程内,控制流前面的操作 Happens-Before 于控制流后面的操作。
1
2
3
4// 线程 A
int a = 1; // 操作 A1
int b = 2; // 操作 A2
// A1 hb A2,尽管可能发生重排序,但单线程内的结果一致性得到保证。锁定规则 (Monitor Lock Rule):这条规则涉及 synchronized 关键字(或显式锁 Lock)。一个线程对一个监视器(Monitor)的 解锁(unlock)操作 Happens-Before 于后续任何线程对同一个监视器的加锁(lock) 操作。
1
2
3
4
5
6
7
8
9// 线程 A
synchronized (lockObj) {
data = 10; // 操作 A1 (包含隐式解锁操作 A2)
}
// 线程 B
synchronized (lockObj) {
int temp = data; // 操作 B1 (包含隐式加锁操作 B0)
}
// A2 hb B0,因此 A1 对 data 的写入对 B1 可见。volatile 变量规则 (Volatile Variable Rule):这条规则保证了 volatile 变量的可见性,并不保证原子性。对一个 volatile 变量的 写入(write)操作 Happens-Before 于后续任何线程对同一个 volatile 变量的读取(read) 操作。
1
2
3
4
5
6
7
8
9
10
11// 线程 A
volatile boolean flag = false;
// ...
flag = true; // 操作 A1 (写 volatile)
// 线程 B
while(!flag) {
// 等待
}
// 操作 B1 (读 volatile)
// A1 hb B1,A1对flag的修改对B1立即可见。线程启动规则 (Thread Start Rule):这条规则保证了线程启动时的初始化可见性。在一个线程对象上调用 start() 方法(操作 A) Happens-Before 于该线程中的任何操作(操作 B)。如:线程 A 调用 threadB.start() 之前对共享变量的所有修改,在 threadB 中都是可见的。
线程加入规则 (Thread Join Rule):这条规则保证了线程终止后的数据可见性。如果线程 A 执行 threadB.join() 方法并成功返回,那么线程 B 中的所有操作(操作 A) Happens-Before 于线程 A 从 threadB.join() 方法返回(操作 B)。
传递性 (Transitivity):Happens-Before 关系是可传递的偏序关系。如果操作 A Happens-Before 操作 B,且操作 B Happens-Before 操作 C,那么操作 A Happens-Before 操作 C。
final 字段规则:对象构建过程中对 final 字段的写入 Happens-Before 于将对象的引用发布给其他线程(只要构造函数正确执行且没有“逸出”)。
内存屏障(Memory Barriers)
Java内存模型(JMM)中的 内存屏障(Memory Barriers) 是一种底层指令,用于强制处理器执行特定的内存操作顺序,并确保不同CPU核心之间的缓存数据一致性。编码工作中通常不需要直接使用内存屏障,而是通过使用Java并发关键字(如 volatile、synchronized)来间接利用它们,JVM编译器在生成机器码时会自动插入适当的内存屏障指令,以确保遵循JMM的 Happens-Before 规则。Java内存模型中内存屏障主要解决两个问题:
- 防止指令重排序(Reordering): 现代编译器和处理器为了优化性能,会重排指令执行顺序。内存屏障会像一道“栅栏”,阻止特定类型指令越过屏障,从而维护程序的有序性。
- 强制缓存同步(Cache Synchronization):确保一个线程对共享变量的修改能够及时地从其本地工作内存(CPU缓存)刷新到主内存,并且其他线程能够从主内存读取到最新值,从而保证可见性。
根据操作类型(加载 Load - 读取,存储 Store - 写入),JMM抽象出四种主要的内存屏障类型:
| 屏障类型 | 作用描述 |
|---|---|
| LoadLoad | 确保屏障前的所有读操作(Load)都在屏障后的所有读操作之前完成并发可见。 |
| StoreStore | 确保屏障前的所有写操作(Store)都在屏障后的所有写操作之前完成并发可见(刷新到主内存)。 |
| LoadStore | 确保屏障前的所有读操作都在屏障后的所有写操作之前完成。 |
| StoreLoad | 确保屏障前的所有写操作都在屏障后的所有读操作之前完成。这是最昂贵的屏障,因为它强制刷新写缓冲区并使其他CPU缓存失效。 |
内存屏障在并发原语中的应用:
- volatile 变量:
- 写入 volatile 变量时: JVM 会在写入操作前插入 StoreStore 屏障,并在写入操作后插入 StoreLoad 屏障。这确保了在写入 volatile 变量之前的所有普通写入操作都已完成,并且该写入操作对后续任何线程的读取都立即可见。
- 读取 volatile 变量时: JVM 会在读取操作后插入 LoadLoad 和 LoadStore 屏障。这确保了读取操作总是获取最新值,并且该读取后的任何操作都不会被重排序到读取操作之前。
- synchronized(锁):
- 进入 synchronized 块(获取锁): 相当于执行了一个 acquire 操作,通常会插入一个全屏障(Full Fence),确保后续操作不会被重排序到锁获取之前,并从主内存同步数据。
- 退出 synchronized 块(释放锁): 相当于执行了一个 release 操作,也会插入一个全屏障,确保屏障前的所有操作(对共享变量的修改)都刷新到主内存,对其他线程可见。
总之:内存屏障是JMM底层实现可见性和有序性的关键机制,它弥合了Java语言规范与不同硬件平台内存模型之间的差异。
MESI 协议(Modified, Exclusive, Shared, Invalid Protocol)
MESI含义:
M(Modified,已修改):MESI第一个字母M,代表着CPU当前L1 cache中某个变量i的状态被修改了,而且这个数据在其他核心中都没有。E(Exclusive,独占):说白了就是CPUA将数据加载自己的L1 cache时,其他核心的cache中并没有这个数据,所以CPUA将这个数据加载到自己的cache时标记为E。(S:Shared,共享):说明CPUA在加载这个数据时,其他CPU已经加载过这个数据了,这时CPUA就会从其他CPU中拿到这个数据并加载到L1 cache中,并且所有拥有这个值的CPU都会将cache中的这个值标记为S。(I:Invalidated,已失效):当CPUA修改了L1 cache中的变量i时,发现这个值是S即共享的数据,那么就需要通知其他核心这个数据被改了,其他CPU都需要将cache中的这个值标为I,后面要操作的时,必须拿到最新的数据在进行操作。
JMM是一个语言级别的规范,定义了多线程编程的规则,JMM 通过 内存屏障(Memory Barriers) 作为桥梁,将抽象的保证转换为具体的硬件操作,而MESI协议就是执行这些硬件操作的底层机制之一。
当Java代码中使用 volatile 或 synchronized 等关键字时,JVM 编译器会插入特定的内存屏障指令(如 x86 架构下的 lock 前缀指令)。这些硬件指令会触发CPU使用其内部的缓存一致性协议(如MESI)来协调内存访问:
- 保证可见性:当一个线程写入一个 volatile 变量时,JVM会插入一个屏障,强制将工作内存(CPU缓存)中的修改数据刷新回主内存。在硬件层面上,这会触发一个 写失效(Write Invalidate) 操作(通过总线命令),使其他所有CPU缓存中对应的缓存行副本变为失效(Invalid) 状态。当其他线程尝试读取该变量时,发现本地缓存行为失效状态,就会强制从主内存或拥有最新数据的其他核心缓存中重新加载最新值,从而保证了可见性。
- 保证有序性:内存屏障阻止了编译器和处理器的指令重排序。在硬件层面,这确保了在屏障之前的内存操作先于屏障之后的操作执行,满足了JMM的有序性要求。
volatile
Java中的 volatile 关键字是Java内存模型(JMM)提供的一种轻量级的同步机制,它主要用于保证共享变量的可见性(Visibility)和有序性(Ordering),但不保证原子性。volatile 在实现上是通过插入 内存屏障(Memory Barriers) 来确保对变量的读写操作能够直接与主内存交互,并防止指令重排序。
1 | |
查看字节码:
1 | |
上述字节码中,并没有看到 volatile,这是因为 volatile 关键字是一个字段修饰符,它不是一个字节码指令。它告诉 Java 虚拟机(JVM),在执行 putfield(写入)或 getfield(读取)这个特定字段(#7)时,必须遵守特殊的内存可见性和有序性规则,即插入内存屏障,防止指令重排序。字节码指令本身(putfield)保持不变,但是 JVM 在解释和执行这条指令时,会检查字段的 volatile 标记,并采取与非 volatile 字段不同的操作。因此,volatile 关键字的魔力在于 JVM 如何执行 putfield 指令,而不是在于生成不同的指令。
CPU缓存一致性协议(MESI协议)对volatile支持 - 硬件层面
查看上述测试代码的汇编码(重点关注update()):
1 | |
从上面可以看到,当写入 volatile 变量通常会使用一个特殊的汇编指令,例如,带有 lock 前缀的指令,如 lock addl 或直接是 movl 伴随内存屏障。在硬件层面发生如下操作:
- 强制刷新缓存: lock 前缀指令会导致CPU缓存中的数据被立即写回主内存。
- 总线嗅探与失效: 这个写入操作会产生一个总线信号,其他CPU核心通过“总线嗅探”机制监听到这个信号。
- 缓存失效: 其他CPU核心会检查自己本地缓存中是否有这个变量的副本,如果有,则标记为“失效”(Invalidated)。
- 重新加载: 当其他线程再次尝试读取该变量时,发现本地缓存已失效,被迫从主内存中重新加载最新数据,从而实现了可见性。
内存屏障对volatile支持 - JVM层面
内存屏障是一种特殊的CPU指令,它可以防止编译器和处理器对指令进行重排序。JVM在对volatile变量进行读写时,会插入特定类型的内存屏障,以确保操作的顺序性。
- 对volatile变量进行写操作时:
- 写入前屏障(StoreStore Barrier):确保在写volatile变量之前,所有普通写操作都已完成,并且不会被重排到volatile写之后。
- 写入后屏障(StoreLoad Barrier):这个屏障会刷新工作内存(CPU缓存)中的数据到主内存,并使其他CPU缓存中该变量的副本失效。它确保在写volatile变量之后,其后的任何读操作都不会被重排到volatile写之前。
- 对volatile变量进行读操作时:
- 读取后屏障(LoadLoad Barrier):确保在读volatile变量之后,其后的任何普通读操作都不会被重排到volatile读之前。
- 读取后屏障(LoadStore Barrier):确保在读volatile变量之后,其后的任何普通写操作都不会被重排到volatile读之前。
volatile特性实现
volatile特性:对一个 volatile 字段的写入操作(Write), Happens-Before(先行发生) 于后续任何线程对同一个 volatile 字段的读取操作(Read)。
volatile在底层实现上是通过 软件层面(JVM)和硬件层面(CPU) 协同工作的过程。JVM通过插入内存屏障来限制指令重排,并结合CPU的缓存一致性协议(通过lock指令)来确保跨线程的内存可见性,从而为开发者提供了轻量级的线程同步保障。它解决了共享变量的可见性与有序性,但并不完全保证原子性,只能保证单个读/写操作的原子性,但对于复合操作(如 i++,它包含“读-改-写”三个独立步骤)则无法保证原子性。
- 解决可见性问题:
- 写操作:volatile写操作通过内存屏障和缓存一致性协议,强制将修改后的值立即写回主内存,并使其他线程的缓存副本失效。
- 读操作:volatile读操作会先检查缓存副本是否失效,如果失效则强制从主内存重新读取最新值,从而保证了读取到的是最新的数据。
- 解决有序性问题:
- 禁止重排:JVM插入的内存屏障确保了volatile操作不会被编译器或处理器重排。
- happens-before保障:
- volatile写:一个线程对volatile变量的写入,happens-before后续任何线程对该变量的读取。这意味着,在volatile写入之前的所有操作(包括对非volatile变量的写入),其结果都会对后续读取该volatile变量的线程可见。
- volatile读:一个线程对volatile变量的读取,happens-before后续任何线程对该变量的写入。这意味着,在volatile读取之后的所有操作,其结果都会看到volatile读取时的最新值。
volatile对性能的影响
volatile 关键字在多线程编程中用于解决变量的可见性和有序性问题,但它会对性能产生一定的影响。volatile对性能的影响主要表现在四个个方面:
- 禁止编译器优化:volatile 会告诉编译器该变量可能会被程序之外的因素(如硬件、中断、其他线程)修改,因此编译器不能对变量的读写操作进行优化(如缓存、指令重排)。
- 强制从主内存读写:每次访问 volatile 变量时,编译器必须生成代码直接从主内存读取或写入,而不是使用寄存器或 CPU 缓存;
- 指令重排限制:volatile 会插入内存屏障(Memory Barrier),禁止编译器和处理器对 volatile 变量的操作进行重排序,这会增加额外的指令开销。
- 内存屏障的开销:volatile 的底层实现依赖于内存屏障(Memory Barrier),它会强制处理器刷新缓存或无效化其他核心的缓存行。
- 写操作更慢:volatile 写操作需要将数据立即刷新到主内存,而普通变量可能在 CPU 缓存中停留更长时间;
- 多核处理器的同步开销:在多核系统中,volatile 写操作可能导致其他核心的缓存行失效,触发缓存一致性协议(如 MESI 协议),增加总线流量和延迟。
- 频繁访问的性能瓶颈:如果 volatile 变量被频繁读写,每次操作都绕过缓存直接访问主内存,会导致性能显著下降。
- 主内存访问速度比缓存慢:主内存的访问延迟远高于 CPU 寄存器或 L1/L2 缓存(例如,L1 缓存访问延迟约为 1-3 纳秒,而主内存访问延迟约为 100-200 纳秒)。
- 高并发下的竞争:多个线程频繁读写 volatile 变量可能导致缓存行竞争(False Sharing),进一步降低性能。
- 缓存行竞争:多个线程频繁修改不同 volatile 变量时,可能因缓存行竞争(False Sharing)导致性能下降。通过填充字段(Padding)对齐缓存行,避免竞争。
总结
在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明,在这种隐式消息传递实现中必须要保证可见性、原子性、有序性,从而保证并发的安全性。在实现上,JMM通过定义主内存与工作内存的交互,并使用Happens-Before规则和内存屏障等机制,为Java并发编程提供了可靠的内存可见性和有序性保证,使开发者能够编写出平台一致的、线程安全的程序。