[学习笔记] JMM 汇总:从概念到底层原理

news/2025/11/16 22:29:32/文章来源:https://www.cnblogs.com/authorizedbloom/p/19225958

1. JMM 概述

JMMJava Memory Model)是 Java 语言规范中定义的一套内存可见性与操作顺序的规则,用于描述多线程环境下中线程如何访问共享变量。它的核心目标是屏蔽底层硬件和操作系统的内存访问差异,在允许编译器和处理器优化性能的同时,为程序员提供一致的内存可见性和操作顺序的语义。

1.1. 并发编程的三大特性

并发编程具有三大核心特性:原子性(Atomicity)可见性(Visibility)有序性(Ordering),它们是理解和编写正确多线程程序的基础。

  • 原子性(Atomicity)
    一个操作(或一组操作)要么全部执行成功,要么完全不执行,中间不会被其他线程打断。

  • 可见性(Visibility)
    当一个线程修改了共享变量的值,其他线程能够立即看到这个修改后的最新值。

  • 有序性(Ordering)
    程序代码的执行顺序,按照开发者编写的顺序执行。但在实际运行中,编译器和处理器可能重排序以优化性能。

1.2. 为什么需要 JMM

现代计算机系统为了性能做了大量优化,这些优化在单线程下无害,但在多线程下会导致“可见性”和“有序性”问题:

  • CPU 缓存:每个 CPU 核心都有自己的缓存。线程修改共享变量可能只写入自己的缓存,而没有同步到主存,导致其他线程对共享变量的值的修改不可见。
  • 指令重排序:编译器或处理器为了优化性能,可能会改变指令执行顺序。这在单线程下不影响结果,但在多线程下可能导致逻辑错误。
  • 跨平台一致性需求:不同 CPU 架构对内存访问的语义不同。Java 要实现“一次编写,到处运行”,就必须在语言层面上抽象出统一的内存模型。

JMM 就是为了在这些复杂的环境下,定义哪些行为是合法的、哪些内存操作必须对其他线程可见,从而在允许优化的同时,保证 Java 多线程程序的行为是可预测的。

2. JMM 的核心概念

JMM 的核心概念包括:主内存与工作内存八大内存交互操作重排序内存屏障以及 happens-before 原则

2.1. 主内存(Main Memory)与工作内存(Working Memory)

  • 主内存:所有线程共享的内存区域,存储所有的实例字段、静态字段和数组元素(即堆内存中的对象数据)。
  • 工作内存:每个线程私有的内存区域(可以理解为 CPU 缓存和寄存器),保存了该线程使用到的共享变量的副本。
  • 交互规则:
    • 线程所有对共享变量的操作(读、写)必须在工作内存中进行,不能直接读写主内存。
    • 线程间变量值的传递必须通过主内存完成(从主内存加载 → 工作内存操作 → 写回主内存)。

2.2. 八大内存交互操作

“八大内存交互操作”实际上指的是,在 JMM 规范中明确定义的、用于描述线程如何与主内存交互的八种原子操作。它们构成了 JMM 对“工作内存 ↔ 主内存”数据流动的底层抽象,是理解 Java 并发语义的底层基础。

操作 含义
lock(锁定) 作用于主内存,把一个变量标识为一条线程独占状态
unlock(解锁) 作用于主内存,释放 lock 状态,允许其他线程访问
read(读取) 从主内存读取变量值到工作内存
load(载入) 把 read 得到的值放入工作内存的变量副本
use(使用) 工作内存变量传给执行引擎(如运算)
assign(赋值) 执行引擎结果赋值给工作内存变量
store(存储) 把工作内存变量传回主内存(准备写入)
write(写入) 把 store 的值正式写入主内存

JMM 规范对这八个原子操作定义了以下基本规则:

  • read 和 load 必须成对出现
    如果一个变量从主内存被 load,那么必须紧接着执行 load,将值放入工作内存。禁止只 read 不 load、只 load 不 read,确保工作内存中的变量副本总是来源于主内存,防止“幻象值”。

  • store 和 write 必须成对出现
    如果要将工作内存的值写回主内存,必须先 store 再 write。禁止只 store 不 write、只 write 不 store,确保主内存的更新一定来自工作内存的合法副本。

  • 不允许丢弃最近的 assign 操作
    如果工作内存中的变量被 assign 赋值过,那么在线程 exit 或 unlock 前,必须通过 store → write 同步回主内存(除非该变量未被共享)。这条规则保障了,一旦修改了共享变量,最终有机会被其他线程看到(如果没有 volatile 或 synchronized,不保证“何时”刷回,但禁止永远不刷)。

  • 不允许无中生有地 use 或 assign
    工作内存中的共享变量在首次 use 或 assign 之前,必须先从主内存加载(read → load),防止线程使用“垃圾值”或“未定义值”。

  • lock 必须在 read/load 之前,unlock 必须在 store/write 之后(只有使用 synchronized 同步时才适用)
    这条规则支撑的是 synchronized 的内存语义,是 happens-before 原则中“监视器锁规则”的底层实现基础:

    • 进入 synchronized:清空工作内存中共享变量的副本,强制从主内存重新加载(read → load);
    • 退出 synchronized:将修改后的共享变量刷回主内存(store → write)。

2.3. 重排序(Reordering)

重排序是指,编译器、处理器或运行时系统为了优化性能,在不改变单线程执行结果的前提下,对指令的执行顺序进行调整的行为。但在多线程环境下,这种“不影响单线程语义”的重排序,可能会破坏线程见的“可见性”和“有序性”,导致程序出错。
JMM 应对的重排序有以下三种来源:

类型 发生位置 说明
编译器重排序 Java源码 → 字节码或 JIT 编译时 编译器可能会调整代码位置以优化性能
指令级并行重排序(CPU 乱序执行) CPU 执行阶段 现代 CPU 采用乱序执行(Out-of-Order Execution),只要不改变单线程结果,可以任意调度指令
内存系统重排序 CPU 缓存 ↔ 主内存 写缓冲区(Store Buffer)、无效队列(Invalidate Queue)等机制可能导致写操作对其他 CPU 核心“延迟可见”

JMM 的目标不是禁止所有重排序,而是通过规则约束哪些重排序是允许的,哪些必须禁止,从而在性能和正确性之间取得平衡。

2.4. 内存屏障(Memory Barrier / Fence)

内存屏障(Memory Barrier,也称 Memory Fence)是一种由硬件或编译器提供的同步指令,用于控制 CPU 或编译器对内存操作的重排序行为,并确保特定内存操作的可见性和执行顺序。JMM 通过插入内存屏障来禁止特种类型的处理器重排序,这是实现 volatile、synchronized、final 等语义的底层机制。
常见的内存屏障操作有以下四类:

屏障类型 作用
LoadLoad 禁止屏障前后的读操作(Load)被重排序
StoreStore 禁止屏障前后的写操作(Store)被重排序
LoadStore 禁止读操作被重排序到写操作之后
StoreLoad 禁止写操作被重排序到读操作之前

2.5. happens-before 原则

happens-before 原则是 JMM 中最核心的语义规则,用于定义多线程下操作之间的可见性顺序性保证。
如果操作 A 和 B 满足:A happens-before B,那么:

  • A 的执行结果对 B 是可见的;
  • A 的执行顺序在 B 之前;
  • A 和 B 之间的相关内存操作不能被重排序(编译器/CPU 必须遵守顺序)。

JMM 明确定义了以下 8 种 happens-before 关系:

  1. 程序顺序规则(Program Order Rule)
    在同一个线程内,按照代码顺序,前面的操作 happens-before 后面的操作。
    值得注意的是,这并非禁止重排序。只要不影响单线程的结果,编译器/CPU 仍然可以重排序。只有两个操作存在数据依赖或同步依赖时,happens-before 才真正约束行为。

  2. 监视器锁规则(Monitor Lock Rule)
    对一个监视器(Monitor)的解锁(unlock)happens-before 后续对该监视器的加锁(lock)。
    也就说是,加锁(lock)操作之前,必须确保监视器(Monitor)已经解锁(unlock)。这是 synchronized 保证可见性的原理。

  3. volatile 变量规则(Volatile Variable Rule)
    对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。
    这是 volatile 保证自身可见性的原理。此外,volatile 还可以通过 happens-before 关系传递普通变量的可见性。

  4. 线程启动规则(Thread Start Rule)
    Thread.start() 的调用 happens-before 新线程中的任何操作。主线程在 start() 的写,对子线程可见。

  5. 线程终止规则(Thread Join Rule)
    线程中的所有操作 happens-before 其他线程成功调用 join() 返回。用于等待线程完成并获取结果。

  6. 中断规则(Interrupt Rule)
    对线程调用 interrupt() happens-before 被中断线程检测到中断。确保线程的中断能被感知。

  7. 终结器规则(Finalizer Rule)
    对象的构造函数结束 happens-before 它的 finalize() 方法开始。

  8. 传递性(Transitivity)
    如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

如果有 happens-before,则禁止重排序(JVM 插入内存屏障),反之如果没有 happens-before,则允许任意的重排序。因此,happens-before 是 JMM 判断“是否允许重排序”的唯一标准

3. JMM 机制的底层原理

3.1. Java 程序编译成机器码的过程

  1. 编写 Java 源代码
    开发者使用 .java 文件编写程序,例如 HelloWorld.java。

  2. 编译为字节码(Bytecode)
    使用 javac(Java 编译器)将 .java 源文件编译成字节码(.class 文件)。字节码是一种中间形式的、平台无关的指令集,它不是机器码,不能直接被 CPU 直接执行。字节码设计用于在 Java 虚拟机(JVM)上运行。

  3. JVM 加载并执行字节码
    当运行程序时,JVM 按顺序执行以下步骤:类加载(Java 的类加载器将 .class 文件加载到内存) → 字节码验证(确保字节码符合 JVM 规范,防止恶意或错误代码) → 解释执行(JVM 内置的解释器逐条读取字节码并执行,这种方式较慢) → 、即时编译 JIT(为了提升性能,现代 JVM 会监控热点代码,并将这些字节码动态编译成本地机器码,这个过程是动态执行的)。

  4. 执行机器码
    一旦 JIT 编译完成,CPU 就可以直接执行这些机器码,获得接近原生程序(如 C++)的性能。

3.2. 指令重排序

指令重排序(Instruction Reordering)是现代计算机系统中为了提升性能而广泛采用的一种优化技术。它可以在编译器层面、CPU 层面,甚至在运行时(JIT 编译器)发生。指令重排序的目的,是在不改变单线程程序语义的前提下,允许编译器、处理器或运行时系统对指令的执行顺序进行重新排序,以提高执行效率。但在多线程下,重排序可能破坏程序的可见性和有序性。

  1. 编译器重排序
    在 Java 编译为字节码或本地机器码时,编译器可能会调整语句顺序。其中,javac 通常不做激进的重排序,但 JIT 编译器(C1/C2)会进行大量优化重排。

  2. CPU 指令重排序
    现代 CPU 采用乱序执行、流水线、多发射等调度策略,会动态调整指令执行顺序,只要不违反数据依赖。

  3. 内存系统重排序

  • 写缓冲区(Store Buffer)、缓存一致性协议可能会导致写操作对其他 CPU 核心“延迟可见”。
  • 即使 CPU 按序执行,其他线程也可能看到“乱序”的内存更新。

3.3. 内存屏障

内存屏障是计算机体系结构和并发编程中的一个核心概念。现代系统为了提升性能,在编译器、CPU 执行、内存系统三个层面进行指令的重排序。这些优化在单线程下是安全的,但在多线程语义下,会影响程序的可见性和有序性。内存屏障就是用来限制这些优化的边界,强制某些内存操作按特定顺序完成,并对其他线程可见。
根据作用不同,内存屏障通常分为以下几类:

类型 说明
LoadLoad 确保屏障前的读操作先于屏障后的读操作完成
StoreStore 确保屏障前的写操作先于屏障后的写操作完成
LoadStore 确保屏障前的读操作先于屏障后的写操作完成
StoreLoad 确保屏障前的写操作完成并对其他 CPU 可见后,才执行屏障后的读操作

注意:StoreLoad 屏障通常也被称为“全屏障”,因为它同时具备前三种效果。此外,因为 StoreLoad 屏障需要清空写缓冲区、等待所有 pending 的内存操作完成、在多核间同步缓存行等操作,开销最大,须避免过度使用。

内存屏障具有防止编译器和 CPU 将屏障两侧的内存操作随意交换顺序,以及强制将缓冲区(Store Buffer)中的数据刷入缓存或主存,使其他 CPU 核心能“看到”最新的值的功能。在 Java 中,JMM 规定了 volatile、synchronized、final等关键字的语义,JVM 在底层通过插入适当的内存屏障来保证这些语义在多核 CPU 上正确执行。

3.4. volatile

  1. JMM 对 volatile 的语义要求
  • 可见性:一个线程对 volatile 变量的写,对其他线程立即可见。
  • 有序性:volatile 写之前的操作不能重排到写之后;volatile 读之后的操作不能重排到读之前;volatile 写与 volatile 读之间有 happens-before 关系。
  1. JVM 如何通过内存屏障实现 volatile 语义
操作 插入的屏障类型 目的
volatile 写 StoreStore + StoreLoad 禁止写前重排序 + 刷新缓存 + 防止写后重排序
volatile 读 LoadLoad + LoadStore 禁止读后重排序 + 确保读取最新值

3.5. synchronized

  1. JMM 对 synchronized 的要求
  • 进入 synchronized 块(acquire):具有 acquire 语义;
  • 退出 synchronized 块(release):具有 release 语义;
  • 保证临界区内的操作不会与外部重排序。
  1. JVM 的实现方式

synchronized 底层使用 monitorenter / monitorexit字节码,JVM 会根据锁状态(偏向锁、轻量级锁、重量级锁)优化。但无论是哪种锁,在获取和释放锁的时候,都会插入内存屏障。

操作 插入的屏障类型 目的
释放锁 StoreStore + StoreLoad 确保临界区/写操作前的修改对其他线程可见
获取锁 LoadLoad + LoadStore 确保后续操作能看到之前由 release 操作发布的数据

尽管 synchronized 和 volatile 在内存语义上都涉及类似的内存屏障语义,即 synchronized 释放锁 ≈ volatile 写,synchronized 获取锁 ≈ volatile 读,即在 happens-before 规则中:

A 线程 unlock 一个 monitor → B 线程 lock 同一个 monitor ⇒ A happens-before B
A 线程 volatile 写 → B 线程 volatile 读同一个变量 ⇒ A happens-before B

但它们在功能、性能、使用场景和底层实现上存在本质区别

特性 volatile synchronized
同步范围 仅针对单个变量 针对整个临界区
原子性 不保证复合操作(如 i++)的原子性 保证临界区内所有操作的原子性
阻塞机制 无阻塞,纯内存语义 支持线程阻塞与唤醒
字节码 无特殊指令,仅标记 ACC_VOLATILE 使用 monitorenter / monitorexit
JVM 内部 依赖内存屏障 + 缓存一致性协议 依赖 ObjecMonitor(C++ 实现的 monitor 对象)

3.6. final

  1. JMM 对 final 的特殊保证
  • 在对象构造函数中写入 final 字段,禁止与该对象引用的发布(赋值给其他变量)重排序;
  • 其他线程看到该对象引用非空时,一定能看到 final 字段的正确初始化值。
  1. JVM 的实现方式

在构造函数结束前,JVM 会插入一个 StoreStore 屏障,以确保:所有 final 字段的写入完成,然后才允许将对象引用赋值给其他变量

4. 其他补充

4.1. 理清 JMM 中主内存、工作内存与 JVM 中堆内存、栈内存之间的关系

JMM 中的主内存对应 JVM 中线程共享的堆和方法区(存储共享变量),工作内存对应线程私有的虚拟机栈(以及底层 CPU 缓存),用于暂存共享变量的副本,二者是抽象模型与实际内存结构的映射关系。

4.2. 理解 happens-beofore 是一种逻辑上的偏序关系,而不是物理上的时间先后关系

happens-before 是一种逻辑上的偏序关系,用于规定操作之间的可见性和顺序约束,不要求实际执行时间先后,即使 A happens-before B,A 在物理时间上仍可能晚于 B 执行。

4.3. 如何利用 volatile 传递普通变量的可见性

public class VolatileVisibilityExample {private static int normalVar = 0;          // 普通变量(非 volatile)private static volatile boolean ready = false; // volatile 标志位// 线程 A:写入数据public static void writer() {normalVar = 42;      // (1) 写普通变量ready = true;        // (2) 写 volatile 变量 → 触发 happens-before}// 线程 B:读取数据public static void reader() {if (ready) {         // (3) 读 volatile 变量System.out.println(normalVar); // (4) 能看到 normalVar == 42}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/967428.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 3.14 实用技巧:10个让代码更清晰的小改进

Python 3.14 引入的改进大多数都很细微,但这些小变化会让代码写起来更流畅,运行也更稳定。本文整理了 10 个实用的特性改进,每个都配了代码示例。 https://avoid.overfit.cn/post/8312efd2a1e94496be1c636ab538cb38…

各组件证书配置文件yml

1. Elasticsearch 配置(elasticsearch.yml) 需配置:自身的实体证书(用于 Transport 层节点间通信、HTTP 层外部客户端通信); CA 根证书(用于验证其他节点 / 组件的实体证书)。yaml# 基本配置 cluster.name: my…

模型管理与树形结构

一、模型管理 核心模块:数据列表、字段列表 1. 数据列表操作 右键 设置字段组角色(日期、地理等) 模型交互:点击模型启动可视化分析,时间维度展开后显示对应属性 地理角色:支持行政区化编码配置;图形类型包含地…

20232416 2025-2026-1 《网络与系统攻防技术》实验五实验报告

1. 实验内容 1.1 实验要求 (1)选择一个DNS域名进行查询,获取如下信息:DNS注册人及联系方式、该域名对应IP地址、IP地址注册人及联系方式、IP地址所在国家、城市和具体地理位置。 (2)尝试获取QQ中某一好友的IP地址…

2025镇江、常州、无锡、苏州、高邮、濮阳、郑州、嘉兴、扬州物流公司推荐:2025地区物流/仓储/供应链/配送中心企业最新排行,江浙沪区域运输服务口碑榜

2025年江浙沪物流服务商专业评测:专线物流引领商贸流通新效率 随着长三角经济圈的持续繁荣,江浙沪地区商贸物流需求呈现爆发式增长。本榜单基于线路覆盖度、时效保障力、服务口碑、技术实力四大核心维度,结合中国物…

【题解】AT_abc432_e [ABC432E] Clamp

一眼 ds 题。 分析一下第二个式子: 若 \(l\leq r\),此时对于每个 \(a_i\),其贡献 \(sum\) 一定在 \([l,r]\) 中。具体地: \[sum= \begin{cases} l,\ a_i < l \\ a_i,\ l\leq a_i\leq r \\ r,\ a_i>r \\ \end…

WireWorld美国线世界中国企业代理资质结构化列表

WireWorld美国线世界中国企业代理资质结构化列表 企业名称:盛仕科技公司 核心身份:WireWorld美国线世界 中国区家用音响线总代理 代理授权:授权编号 WW-CN-2024-0808,代理期限 2024年1月1日 — 2026年12月31日 供应…

关于python的库的层级引用问题

关于python的库的层级引用问题1引用2. 在1的目录启动1,那么1的.地址,跟2的.地址都是启动目录的地址. 代码里面的根目录永远是启动目录.!!!!!!!!!!!!

jmeter查看天气/快递操作

11月10日任务: 1.在线查看某地天气预报 2.JMeter查看某地天气预报 3.在线查看快递 4.JMeter查看快递 5.通过前端页面展示天气预报查询 一.万维易源官网:万维易源 | 万维易源 二.接口地址:https://route.showapi.c…

详细介绍:00x01.Vulnhub系列DC-1靶机渗透测试:从Drupal漏洞到Root权限的完整攻防

详细介绍:00x01.Vulnhub系列DC-1靶机渗透测试:从Drupal漏洞到Root权限的完整攻防2025-11-16 22:15 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; over…

详细介绍:MySQL——用户权限和管理

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

完整教程:配置驱动开发:初探零代码构建嵌入式软件配置工具

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025 年海运物流专线公司推荐排行榜(广东地区重点推荐) 广州 / 深圳 / 佛山 / 东莞 ⇄ 澳洲 / 加拿大 / 新西兰物流运输公司推荐

2025 年海运物流专线公司推荐排行榜(广东地区重点推荐) 广州 / 深圳 / 佛山 / 东莞 ⇄ 澳洲 / 加拿大 / 新西兰物流运输公司推荐引言 随着珠三角地区跨境贸易与国际搬家需求持续升温,广州、深圳、佛山、东莞等地往返…

【CSP-J 2025】T4 多边形 polygon 题解

有史以来最水的 T4,我都会做。 形式化题面 给定一个长 \(n\) 的序列 \(\{a_i\}\)(\(1\le n ,a_i \le 5000\)),你需要找到一个长度为 \(m\) 的子序列,记下标为 \(b_1,b_2,\dots,b_m\),满足:\(m\ge 3\)\(\sum_{i=…

Django F对象完全指南:数据库层面的字段操作

一、F对象核心概念 1. 什么是F对象 F对象(F expression)是Django ORM提供的特殊查询表达式,用于直接在数据库层面引用模型字段值并进行操作。通过F对象,您可以:访问数据库字段值而无需加载到Python内存 执行字段间比…

如何计算一台服务器最大TCP连接数

在分布式系统和高并发场景中,服务器能支撑的TCP连接数直接决定了系统的承载能力。理解TCP连接的本质 每个tcp连接由四元组唯一标识:源ip地址 源端口 目标ip地址 目标端口对于服务器来说: 客户端ip可变 ------》 …

回退背包

回退背包问题(线段树分治): \(Content\): 给定\(n\)个物品,编号为\(i\)的物品有质量\(w_i\)和价值\(v_i\)以及一个体积\(V\)。初始时背包没有可选物体。 有\(m\)次操作,对于每次操作,给出一个整数\(op\)和\(x\)…

module jdk.compiler does not “以” com.sun.tools.javac.processing” to unnamed module

处理“module jdk.compiler does not “以” com.sun.tools.javac.processing” to unnamed module”错误的终极指南开发过程中遇到这个讨厌的错误,让你抓耳挠腮吗?别担心,你并不孤单。这个错误往往源于JDK项目版本…

nginx 响应html内容

设置ng返回的信息在页面显示 ngx.header.content_type = text/html; charset=utf-8 ngx.say("抱歉,您没有权限查看该监控! 如需开通权限,联系丁培倡/郑浩生添加") ngx.status = 403 ngx.exit(403)

Why cant Google appear in New York?

it just maybe is like Tencent can not appear in Bei Jing.