JMM(Java内存模型)

JMM核心概念

  1. JMM定义与作用

JMM(Java Memory Model)是Java虚拟机定义的多线程环境下共享变量访问的规范, 解决了因硬件差异(缓存一致性、指令重排序等)导致的跨平台并发问题,为开发者提供了清晰的多线程编程语义。

  1. JMM与JVM内存结构的区别

很多人会混淆JMM(Java 内存模型)和JVM内存结构,实则二者有着本质区别:

  • JVM内存结构聚焦于物理层面的内存布局,它定义了Java程序运行时内存的具体划分方式(如方法区、堆、虚拟机栈等);
  • JMM则是逻辑层面的并发规范,它不涉及实际的内存分配细节,而是通过定义线程、工作内存、主内存之间的抽象交互规则,解决并发编程中的数据竞争问题。

JMM的必要性

屏蔽底层硬件差异

  1. 问题背景: 不同的处理器架构(如 x86 vs ARM/Power)和编译器优化策略,在内存访问顺序、缓存一致性、指令重排序以及原子操作支持等方面存在显著差异。
  2. 带来的风险: 同一段Java多线程代码在不同平台运行时表现出不一致的行为。
  3. JMM解决方案: JMM定义了一个抽象的内存模型,规定了多线程环境下线程如何与内存交互的规则。

解决并发三问题

  1. 问题背景: 多线程并发访问共享数据会带来三个经典并发难题------可见性、有序性、原子性。
    • 可见性:一个线程修改了共享变量的值,其他线程是否能立即看到这个修改?
    • 有序性:编译器和处理器会对指令进行重排序以提高性能,程序代码是否按预期顺序执行?
    • 原子性:一个或多个操作在CPU执行时是否能保证不可分割?
  2. JMM解决方案: JMM通过三大机制解决并发问题。
    • 内存模型:主内存与工作内存的隔离和同步。
    • 操作规范:定义八大原子操作、happens-before原则。
    • 关键字实现:volatile、synchronized、final及原子类,底层通过插入内存屏障实现。

JMM内存模型

  1. 核心组件

JMM内存模型将内存分为主内存和工作内存。

  • 主内存:存储所有的共享变量(实例字段、静态字段等)。线程不能直接读写主内存,必须通过工作内存。
  • 工作内存:每个线程私有的内存空间,存储该线程使用到的变量副本。不同线程的工作内存相互隔离,不能直接访问。
  1. 硬件映射关系

JMM 是一个抽象的内存模型,其核心语义通过底层物理硬件(如 CPU 缓存、寄存器、系统内存)及 JVM 的内存屏障机制共同实现。

现代JVM实现大多运行在多核处理器上,每个核心通常拥有自己私有的L1和L2缓存,多个核心共享L3缓存。JMM内存模型中的"主内存"主要对应系统内存DRAM,"工作内存"主要对应CPU缓存(L1/L2)和寄存器。

JMM操作规范

八大原子操作

JMM定义了 8 大原子操作,描述了线程与主内存、工作内存之间交互的最小操作单元和规则。

  1. read(读主存):从主内存读取变量的值
  2. load(载入工作内存):将读取到的值存入工作内存的变量副本中
  3. use:把工作内存的变量值传递给线程执行引擎
  4. assign:将执行引擎返回的值赋给工作内存的变量副本
  5. store:把工作内存中的变量值存储到主存通道。
  6. write(写入主存):将store 传输的值更新到主内存的变量中。
  7. lock:当线程进入同步代码块时,JVM会隐式地对该对象加锁。
    • 获取对象的监视器锁(Monitor),触发同步机制。
    • 强制清空工作内存,使该线程后续 read 操作必须从主内存重新加载变量。
    • 插入内存屏障,禁止与后续操作的指令重排序。
  8. unlock:当线程退出同步代码块或方法时,JVM会释放该对象的锁。
    • 将工作内存中所有被修改的变量同步到主内存(通过 store→write)。
    • 释放监视器锁,唤醒其他阻塞线程。
    • 插入内存屏障,防止指令重排序

happens-before原则

JMM 定义了一套明确的内存可见性和操作排序规则(其核心是 happens-before 原则)。

规则 说明
程序顺序规则 单线程内操作按代码顺序
锁规则 解锁(unlock)先于后续加锁(lock)
volatile规则 volatile 变量的写先于后续读
线程启动规则 Thread.start()先于该线程的run()方法
线程终止规则 线程所有操作先于其他线程检测到其终止(如t.join()或t.isAlive())
传递性规则 如果 A happens-before B,B happens-before C,则 A happens-before C
中断规则 thread.interrupt()调用先于中断被检测到(isInterrupted())

JMM实现机制

内存屏障

内存屏障(Memory Barrier)是 JMM 实现多线程内存可见性和指令有序性的底层机制,通过禁止特定类型的指令重排序和强制刷新缓存,为 volatile、synchronized 等关键字的语义提供硬件级支持。JSR-133 规范明确定义了以下四类内存屏障及其作用:

屏障类型 作用 JVM 实现(示例)
LoadLoad 禁止读操作与后续读操作重排序 Load1; LoadLoad; Load2
StoreStore 禁止写操作与后续写操作重排序 Store1; StoreStore; Store2
LoadStore 禁止读操作与后续写操作重排序 Load1; LoadStore; Store2
StoreLoad 全能屏障,禁止所有重排序 Store1; StoreLoad; Load2

底层硬件实现机制: 内存屏障的具体执行依赖CPU架构的底层指令,不同硬件通过特定指令保证屏障语义:例如x86架构通过mfence或lock前缀指令实现内存屏障,ARM架构通过dmb(数据内存屏障)指令实现。

volatile关键字

volatile 是 JMM 提供的一种轻量级的同步机制,用于修饰变量,通过内存屏障指令实现可见性与有序性保障,但不保证原子性。

volatile特性

volatile底层通过插入特定的内存屏障实现两种关键保证:

  1. 可见性保证

当一个线程修改volatile 变量值时,自动执行同步到主内存。 当一个线程读取volatile 变量值时,自动从主内存刷新。

  1. 有序性保证(插入内存屏障)

volatile写:在这个写操作之前的任何读写操作都不能被重排序到volatile写操作之后。 volatile读:在这个读操作之后的任何读写操作都不能被重排序到volatile读操作之前。

注意:volatile仅保证单次读/写本身是原子的,不能保证任何需要多个步骤(读-改-写)的操作是原子的, 最经典的例子就是 i++。

volatile写操作流程

Java 复制代码
volatile int x = 1;
x = 2; // 对应原子操作序列 + 内存屏障

一个 volatile 写操作在 JVM 层面会被编译成以下指令:

  1. assign(x, 2):将值2赋给工作内存中的x副本。
  2. StoreStore 屏障:确保前面所有普通写操作都已完成,禁止与后续volatile写重排序。
  3. store(x):将工作内存中的x=2传输到主内存。
  4. write(x):将x=2写入主内存的实际变量。
  5. StoreLoad 屏障:确保当前volatile写对后续所有读操作可见,禁止与后续读操作重排序。

volatile读操作流程

Java 复制代码
volatile int x;
int y = x; // 对应 read(x) → load(x) → use(x)

一个 volatile 读操作在 JVM 层面会被编译成以下指令:

  1. LoadLoad屏障:确保前面的所有读操作都已完成。
  2. read(x):从主内存读取x的最新值到线程的传输缓冲区。
  3. load(x):将传输缓冲区的值放入工作内存的x副本。
  4. use(x):将工作内存中的x值传递给执行引擎,用于赋值给变量y。
  5. LoadStore屏障:禁止后续的写操作重排序到当前volatile读之前。

底层硬件实现

当线程在CPU Core1上运行,试图读取或修改变量x时:

  1. 如果x在Core 1的L1/L2缓存中有副本,操作会直接在这个副本上进行,即JMM中的工作内存中的变量x的拷贝。
  2. 如果Core1修改了x,这个修改首先发生在缓存副本中,在没有任何同步机制(如volatile或锁)的情况下,这个修改不会立刻写回主内存,其他核心的缓存也不会立即知道这个修改(导致可见性问题)。
  3. volatile写操作会插入内存屏障(如StoreStore + StoreLoad),将修改从Core1的缓存写回主内存,并让其他核心的缓存中该变量的副本失效(通过缓存一致性协议MESI),从而保证其他线程读取时能看到最新值,解决可见性问题。

synchronized关键字

synchornized 是 Java 内置的同步机制,用于确保在同一时刻最多只有一个线程可以执行被其保护的代码块或方法。它通过在JVM层面实现锁机制来实现互斥访问。

核心特性

  1. 可见性保证

问题根源: CPU 缓存架构导致线程的工作内存与主内存中的共享变量副本可能不一致。

synchronized 方案:

  • 退出临界区(释放锁):强制将工作内存中所有修改过的共享变量刷新回主内存 。
  • 进入临界区 (获取锁): 强制使工作内存中相关共享变量的缓存失效,必须从主内存重新加载 。

临界区:被 synchronized 保护的代码块或方法,是访问共享资源的关键区域。synchronized 保证同一时间只有一个线程能执行临界区代码。

  1. 原子性保证

问题根源: 复合操作(如i++)在多线程下被拆分成多个指令执行,发生线程切换导致数据错误。

synchronized 方案: 借助锁的互斥特性(涵盖轻量级锁、重量级锁,以及偏向锁出现竞争后的状态转换),确保整个临界区代码的执行具备原子性 ------ 同一时间仅有一个线程能够进入并执行临界区代码,从而避免多线程交错执行引发的数据错误。

  1. 有序性保证

问题根源: 编译器和处理器指令重排序。

synchronized 方案:

  • 临界区内: 允许重排序,但保证 as-if-serial 语义 (单线程结果正确)。
  • 临界区边界: 通过插入的内存屏障限制临界区内部的读写操作与外部操作之间的特定类型的重排序。

锁升级机制

synchronized锁在JVM层面实现了自适应锁优化,根据竞争情况动态调整锁的级别(无锁->偏向锁->轻量级锁->重量级锁)。

偏向锁
  1. 场景: 适用于几乎没有竞争的场景,锁在大多数情况下由同一线程获得。
  2. **实现:**当对象首次被线程获取锁时, 通过CAS (Compare-And-Swap) 操作将对象头中Mark Word 中的偏向线程 ID 设置为当前线程 ID,同时标记锁状态为"偏向模式"。此后该线程再次进入同步块时,无需重复执行CAS,仅需简单比对Mark Word中的线程ID与自身是否一致。

对象头 & Mark Word: 每个Java对象在内存中的存储分为对象头、实例数据、对齐填充,而对象头的第一部分称为Mark Word。Mark Word 区域是 synchronized 实现的关键。

CAS(Compare-And-Swap):CAS是一种硬件级别的原子操作,通过 "比较 - 交换" 的原子操作实现无锁并发控制。

  1. 原子性保障: 依赖于CAS操作的原子性来更新Mark Word。如果CAS成功,则原子性地获取了偏向锁;如果失败(说明存在竞争或已偏向其他线程),则触发偏向锁撤销。
  2. 可见性保障: 依赖单线程独占性天然保证,无显式内存屏障;仅在初始偏向设置、偏向撤销或锁升级等特殊节点可能引入必要屏障。
  3. 有序性保障: 依赖单线程执行的天然有序性,遵循 "as-if-serial" 语义。
  4. 升级条件: 当其他线程尝试获取锁时,撤销偏向锁,升级为轻量级锁。

从 Java 15 开始,偏向锁默认被禁用,因为其收益在现代硬件和应用场景下往往不明显。

轻量级锁
  1. 场景: 适用于轻度竞争(短时间自旋即可获得锁)。
  2. 实现: 线程获取锁时,会先在自身栈帧中创建锁记录(Lock Record),随后通过 CAS 操作尝试将对象头的 Mark Word 更新为指向当前锁记录的指针
  3. 原子性: 依赖 CAS 操作的原子性实现更新,成功执行 CAS 的线程将原子性地独占轻量级锁,其他线程的 CAS 尝试会失败,确保锁的互斥性,实现临界区代码的原子性(独占执行)。
  4. 可见性: 主要依赖 CPU 的缓存一致性协议(如 MESI)隐式保证锁记录状态的可见性。CAS 操作本身隐含内存屏障语义(通常包含 LoadStore 和 StoreStore 屏障),确保 CAS 操作前后的内存读写不会被重排序,保障锁状态更新结果的跨线程可见性。
  5. 有序性: CAS 操作附带的屏障在一定程度上限制了重排序。轻量级锁临界区内的操作顺序主要依赖线程内的 as-if-serial 语义保障,通过锁的独占性间接保证单线程执行视角下的有序性。
  6. 升级条件: CAS失败(竞争加剧),自旋超过阈值后升级为重量级锁。
重量级锁
  1. 场景: 处理高竞争场景。
  2. 实现: 当锁膨胀为重量级锁时,对象头 Mark Word 会指向堆中的 ObjectMonitor 实例。未获取锁的线程会被放入 _EntryList 队列并由操作系统挂起(进入阻塞状态),直至持有锁的线程释放锁后,再由 ObjectMonitor 从队列中唤醒一个线程竞争锁。

在 Java 并发编程中,monitor(监视器)锁是实现线程同步的核心机制之一,ObjectMonitor 是 HotSpot JVM 中实现 Monitor 锁的核心数据结构,synchronized重量级锁在JVM层面是通过Monitor锁来实现线程同步的。_EntryList是ObjectMonitor的核心字段之一,用于存储阻塞等待锁的线程。

  1. 底层依赖: 需要依赖操作系统的互斥量(mutex)实现同步,每次获取 / 释放锁都会触发用户态到内核态的切换(涉及操作系统内核级别的调度),是三种锁状态中开销最大的。
  2. 原子性: 由操作系统内核提供的互斥量 (mutex) 保证。内核级别的锁机制确保了同一时刻只有一个线程能成功进入临界区,实现了最严格的互斥访问。
  3. 可见性&有序性: 通过在 ObjectMonitor 的进入 (EnterI) 和退出 (Exit) 操作中显式插入全套内存屏障指令,同时保证了可见性和有序性。
    • 进入临界区 (获取锁后): 插入 LoadLoad + LoadStore 屏障。
      • LoadLoad: 确保临界区内的读操作能看到锁释放前(由上一个持有者)对共享变量所做的所有修改(刷新本地缓存/从主存加载最新值)。
      • LoadStore: 确保临界区内的写操作不会被重排序到屏障之前的读操作之前(保证读操作看到的旧值不会被后续写操作覆盖)。
    • 退出临界区(释放锁前): 插入 StoreStore + StoreLoad 屏障。
      • StoreStore: 确保临界区内的所有写操作在释放锁之前对其他线程可见(刷新到主存)。确保释放锁这个"标志"被写入时,它之前的写操作都已完成。
      • StoreLoad: 这是一个全能型屏障(通常开销最大),它确保释放锁对其他线程可见后,任何后续的读操作都能看到临界区内最新的写结果。它也防止临界区内的写操作与释放锁后的读操作重排序。

Java 并发同步机制对比

机制 原子性 可见性 有序性 适用场景
volatile 状态标志
synchronized 高竞争复合操作
AtomicXxx 计数器等简单原子操作
final ✅(正确初始化后) ✅ (初始化安全保证) 不可变对象/常量
  1. volatile: 仅保证单个变量的可见性和有序性 (禁止特定重排序),不保证复合操作原子性。轻量级(无锁,仅内存屏障),开销通常小于 synchronized。
  2. synchronized: 能保证代码块内操作的原子性,并保证该代码块内访问的所有共享变量的可见性和临界区整体的有序性。在高竞争时升级为重量级锁,是功能最全但开销相对较大的同步机制。
  3. 原子类 (AtomicXxx): 利用 CAS 保证单个变量上特定操作的原子性(如 incrementAndGet, compareAndSet),通过volatile关键字保证变量的可见性,借助CAS本身内存语义提供有序性。适用于计数器等简单场景,在低竞争场景下性能优于锁机制,在高竞争下可能因CAS频繁失败导致自旋开销激增。
  4. final: 正确初始化的final字段对其他线程立即可见,无需同步,不保证操作原子性。适用于常量、不可变对象。
  • 关于这几种同步机制选择的流程图如下:
相关推荐
鬼火儿4 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin4 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧5 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧5 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧5 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧5 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧5 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧6 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧6 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang6 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构