当前位置: 首页 > news >正文

第1篇:Java内存模型(JMM)与volatile——并发编程的基石

从一段诡异的代码说起

publicclassVolatileDemo{privatestaticbooleanflag=false;// 不加volatilepublicstaticvoidmain(String[]args){newThread(()->{while(!flag){// 线程B:死循环等待flag变成true}System.out.println("线程B退出");}).start();Thread.sleep(1000);flag=true;// 线程A:1秒后将flag设为trueSystem.out.println("线程A已设置flag=true");}}

你猜结果是什么?在server模式下,线程B永远不会退出——明明线程A已经把flag改成了true,线程B却看不见。这不是CPU缓存的问题吗?但JVM内存模型中的"本地内存"又是什么?

本文核心问题:

  1. Java内存模型(JMM)到底是什么?和JVM内存结构是一回事吗?
  2. 可见性问题是怎么发生的?底层原因是什么?
  3. volatile为什么能保证可见性?内存屏障做了什么?
  4. 指令重排序是什么?volatile如何禁止重排序?
  5. happens-before规则有哪些?怎么用?
  6. DCL单例为什么必须加volatile?
  7. volatile能保证原子性吗?和synchronized的区别?
  8. JMM和硬件内存模型的关系是什么?

读完本文你将彻底理解Java并发编程中最基础也最核心的可见性、有序性、原子性三大问题。


一、JMM到底是什么?和JVM内存结构是两码事

疑问:JMM和JVM运行时数据区(堆、栈、方法区)是什么关系?

回答:这是两个完全不同层面的概念,80%的人会搞混。

JVM内存结构(运行时数据区)——物理划分

这是你在JVM系列学到的:

JVM运行时数据区: ├── 线程私有 │ ├── 程序计数器 │ ├── 虚拟机栈(栈帧:局部变量表、操作数栈) │ └── 本地方法栈 └── 线程共享 ├── 堆(对象实例) └── 方法区/元空间(类信息、常量)

关键词:内存区域物理划分、对象存在哪、GC发生在哪。

JMM(Java内存模型)——并发模型抽象

JMM是一个抽象规范,定义了一套规则:

JMM抽象模型: ├── 主内存(Main Memory)—— 所有线程共享,存放共享变量 ├── 本地内存(Local Memory)—— 每个线程私有,存放共享变量的副本 └── 八个原子操作:lock/unlock/read/load/use/assign/store/write

关键词:并发访问规则、可见性保证、happens-before约束。

举一个例子彻底区分

publicclassUser{privateStringname="张三";// name存储在堆中(JVM内存结构视角)}// 线程Auser.setName("李四");// JMM视角:线程A在本地内存中修改副本,再同步到主内存// 线程BSystem.out.println(user.getName());// JMM视角:线程B从主内存读取(可能是旧值)
  • JVM内存结构回答:name这个字符串对象存在堆里
  • JMM回答:线程B能否看到线程A的修改、什么时候能看到

一句话总结:JVM内存结构管"东西放哪";JMM管"多个线程怎么读写同一个东西"。


二、可见性问题的真相——从CPU缓存到JMM抽象

疑问:为什么线程A改了flag,线程B看不见?到底是CPU缓存还是JMM的锅?

回答:三层递进关系——底层硬件CPU缓存 → JMM规范抽象 → Java代码行为。

你之前写的那篇「线程本地缓存?CPU缓存!」已经点到了核心——“线程没有本地内存”,那是什么?

2.1 硬件层:CPU三级缓存

CPU 核心0 CPU 核心1 │ │ L1 缓存(32KB,私有) L1 缓存(32KB,私有) │ │ L2 缓存(256KB,私有) L2 缓存(256KB,私有) │ │ └──────── L3 缓存 ──────────┘ (共享) │ 主内存(RAM)

可见性问题的硬件根因:每个CPU核心有自己的L1/L2高速缓存。线程A运行在核心0上,修改了变量flag,这个修改可能只停留在核心0的L1缓存里,还没有刷新到主内存。线程B运行在核心1上,从自己核心的L1缓存里读到的还是旧值。

这就是你文章中"线程本地内存其实是CPU缓存"的正确答案——JMM规范里说的"本地内存",在硬件上对应的是CPU的私有缓存。

2.2 JMM层:抽象模型

JMM把这个硬件事实抽象为"主内存"和"本地内存":

JMM抽象模型: 线程A 主内存 线程B ┌──────────┐ ┌──────────┐ ┌──────────┐ │ flag=true│ │ flag=false│ │ flag=false│(不可见) │ (副本) │ │ (主内存) │ │ (副本) │ └──────────┘ └──────────┘ └──────────┘
  • 线程A修改了flag,但还没有写回主内存
  • 线程B从主内存读到的还是false
  • JMM不保证一个线程的修改立即可见于其他线程,除非显式使用同步机制

2.3 Java代码层

privatestaticbooleanflag=false;// 没有volatile// 线程A:flag = true; // 可能只写到CPU缓存,没到主内存// 线程B:while(!flag){} // 可能只读CPU缓存,看不到线程A的修改

三层总结:

Java层: 没有volatile修饰 → JMM不保证可见性 JMM层: 允许线程在本地内存操作 → 不需要立即同步回主内存 硬件层: CPU缓存延迟刷新 → 其他核心看不到

三、volatile如何保证可见性?内存屏障的魔法

疑问:volatile到底做了什么,让flag的修改能被所有线程看见?

回答:volatile通过在指令序列中插入内存屏障(Memory Barrier),强制完成三件事。

3.1 volatile的读写语义

privatestaticvolatilebooleanflag=false;

加上volatile后,JMM规定:

操作语义
volatile写将当前线程本地内存中修改的值立即刷新到主内存
volatile读每次读取都从主内存重新加载,不使用本地内存的缓存值

3.2 内存屏障的种类和作用

JMM定义了四种内存屏障:

屏障类型 指令示例 作用 ────────────────────────────────────────────────── LoadLoad Load1;LoadLoad;Load2 确保Load1在Load2之前完成 StoreStore Store1;StoreStore;Store2 确保Store1在Store2之前完成 LoadStore Load1;LoadStore;Store2 确保Load1在Store2之前完成 StoreLoad Store1;StoreLoad;Load2 确保Store1在Load2之前完成(最重)

volatile的插入规则

// volatile写之前插入 StoreStore 屏障// 确保在写volatile之前,之前的普通写操作全部完成storestore();volatile变量=新值;// volatile写之后插入 StoreLoad 屏障// 确保本次volatile写对后续读可见storeload();// ====================// volatile读之后插入 LoadLoad 屏障// 确保后续普通读操作能读到最新值intval=volatile变量;loadload();// volatile读之后插入 LoadStore 屏障// 确保后续普通写操作不重排到volatile读之前loadstore();

用底层术语讲:内存屏障本质上是一条CPU指令(如x86的mfencelfencesfence),它强制CPU将写缓冲区的数据刷到缓存/内存,并使其他核心的缓存行失效。

3.3 缓存一致性协议(MESI)

volatile除了内存屏障,还依赖CPU的缓存一致性协议

MESI四种状态: M (Modified) : 该缓存行只在本核心,已被修改,需要写回主内存 E (Exclusive) : 该缓存行只在本核心,与主内存一致 S (Shared) : 该缓存行在多个核心,与主内存一致 I (Invalid) : 该缓存行无效,需要从主内存重新读取 volatile写时: → 将本地缓存行状态置为M → 通过总线发送消息,使其他核心的对应缓存行失效(置为I) → 其他核心读取时发现缓存失效,从主内存重新加载

可见性的完整链路

线程A写volatile变量: StoreStore屏障 → 刷新写缓冲区 → CPU发RFO消息 → 其他核心缓存行失效(I) → 写入主内存 → StoreLoad屏障 线程B读volatile变量: 本地缓存失效(I) → 从主内存加载 → LoadLoad屏障 → 读到最新值

四、指令重排序与volatile的有序性保证

疑问:加了volatile就能禁止指令重排吗?什么是重排序?

回答:volatile能禁止特定位置的重排序,但不是禁止全部。

4.1 编译器和CPU的重排序

// 你写的代码a=1;// 1b=2;// 2flag=true;// 3 (volatile写)c=3;// 4d=4;// 5

JMM允许的重排序范围

重排序自由区: a=1 和 b=2 可以互换(1和2之间没有屏障) c=3 和 d=4 可以互换(4和5之间没有屏障) 重排序禁止区: 所有在 volatile写之前的操作,不能重排到volatile写之后 所有在 volatile写之后的操作,不能重排到volatile写之前

本质:volatile就像一个"栅栏",只能管住栅栏两侧的操作不互换,但栅栏同侧的操作依然可以自由重排。

4.2 一个经典例子

// 线程Adata=100;// 1ready=true;// 2 (volatile写)// 线程Bif(ready){// 3 (volatile读)System.out.println(data);// 4}

volatile保证了什么?

  • 1一定在2之前执行(不会被重排到2之后)
  • 3一定在4之前执行(不会被重排到4之后)
  • 所以:线程B看到ready=true时,一定能看到data=100

如果没有volatile修饰ready:1和2可能重排,线程B可能看到ready=truedata=0(data还没赋值)。


五、happens-before规则——JMM的终极法则

疑问:除了volatile,还有哪些情况能保证可见性?

回答:JMM定义了一套happens-before规则,只要满足其中一条,前一个操作的结果就对后一个操作可见。

八大happens-before规则

1.程序次序规则:同一个线程内,前面的代码 happens-before 后面的代码inta=1;// 1intb=2;// 2// 1 happens-before 22.volatile变量规则:volatile写 happens-beforevolatilevolatileintv;v=1;// 写intx=v;// 读,能看到v=13.锁规则:unlock happens-before locksynchronized(obj){a=1;}// 释放锁synchronized(obj){intx=a;}// 获取锁,x一定等于14.传递性:AhbB,BhbCAhbC// 结合规则1+2:volatile写前的所有操作,对volatile读后的所有操作可见5.线程启动规则:Thread.start()happens-before 该线程的run()t.start();// t.run()能看到start()之前的所有修改6.线程终止规则:线程的所有操作 happens-beforejoin()返回 t.join();// join返回后,能看到t线程的所有修改7.线程中断规则:interrupt()happens-before 被中断线程检测到中断 t.interrupt();// t检测到中断时,能看到interrupt()之前的所有修改8.对象终结规则:构造函数执行完 happens-beforefinalize()

happens-before是最重要的并发概念,它是判断"线程B能不能看到线程A的修改"的唯一标准。不满足任何一条规则,就不能保证可见性。


六、DCL单例为什么必须用volatile?

疑问:双重检查锁定(DCL)单例模式,为什么volatile不能省?

回答:因为new操作不是原子的,没有volatile会导致指令重排序,线程可能拿到"半初始化"的对象。

6.1 没有volatile的DCL

publicclassSingleton{privatestaticSingletoninstance;// 没有volatilepublicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();// 问题出在这!}}}returninstance;}}

6.2 new Singleton() 的实际执行过程

JVM将这一行代码分解为三条指令:

memory = allocate(); // 1. 分配内存空间 ctorInstance(memory); // 2. 调用构造函数初始化对象 instance = memory; // 3. 将instance指向分配的内存地址

问题:指令2和3可能被重排序!

memory = allocate(); // 1 instance = memory; // 3(重排后先执行) ← instance已经非null了! ctorInstance(memory); // 2(重排后后执行) ← 但对象还没初始化!

多线程下的灾难

时间线: T1: 进入synchronized,执行new操作 T1: 分配内存 → instance指向内存(但还没初始化) T2: 第一次检查 instance != null → 直接返回instance T2: 拿到一个没初始化的对象!可能NPE或拿到错误的字段值 T1: 初始化对象(已经晚了)

6.3 加volatile解决

privatestaticvolatileSingletoninstance;

volatile禁止了指令2和3的重排序。因为instance = memory是一个volatile写,它之前的ctorInstance(memory)(普通写)不能重排到volatile写之后。

完整的DCL

publicclassSingleton{privatestaticvolatileSingletoninstance;publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();// volatile保证安全}}}returninstance;}}

七、volatile不能保证原子性

疑问:volatile保证了可见性,那i++用volatile安全吗?

回答:不安全。volatile只保证可见性和有序性,不保证原子性。

privatestaticvolatileintcount=0;// 10个线程各执行10000次 count++count=0→ 最终结果 ≠100000(远小于预期)

原因count++不是原子操作,它分为三步:

1. 从主内存读取count的当前值(比如42) 2. 在CPU里执行 42 + 1 = 43 3. 把43写回主内存

并发问题

时间线: T1: 读取 count = 42 T2: 读取 count = 42 ← 两个线程都读到了42 T1: count = 43 ← T1写回 T2: count = 43 ← T2写回,也写43!两次++但值只加了1

volatile能做到什么:T1写回后,T2能立即看到最新值(不会读到过期值),但不能阻止T2在T1写回之前就已经读取了旧值

解决方案

方案用法
synchronizedsynchronized(this) { count++; }
AtomicIntegeratomicInteger.incrementAndGet()
LongAdder高并发计数最优
privatestaticfinalAtomicIntegercount=newAtomicInteger(0);count.incrementAndGet();// CAS保证原子性

八、JMM与硬件内存模型的对应关系

疑问:JMM为什么设计得这么抽象?直接映射硬件不更简单吗?

回答:因为不同CPU架构的内存模型差异巨大,JMM必须"屏蔽硬件差异",提供统一的行为保证。

硬件平台内存模型特点重排序程度
x86/x64强内存模型(TSO)只允许StoreLoad重排
ARM/PowerPC弱内存模型几乎所有重排都可能
SPARCTSO或RMO可选取决于配置

JMM的设计折中

强一致性模型(如x86 TSO): 优点:程序员容易理解,几乎不需要内存屏障 缺点:限制了硬件优化,性能差 弱一致性模型(如ARM): 优点:硬件可以大幅重排,性能好 缺点:程序员需要大量使用屏障,容易出错 JMM的折中: 为Java程序员提供统一的、易理解的happens-before规则 用volatile/synchronized声明需要保证可见性的地方 JVM负责在不同平台上插入对应的内存屏障

同一个volatile,不同平台的实现

// Java代码:volatile写x=1;// JVM在x86上的实现(只禁止StoreLoad重排,用mfence):mov[x],1mfence// JVM在ARM上的实现(禁止所有相关重排,用dmb):dmb ish str r1,[r0]dmb ish

JMM的意义:你只需要写一次volatile,剩下的屏障插入、平台适配全由JVM负责。这就是"一次编写,到处运行"在并发领域的体现。


总结

  • JMM是并发访问的抽象规范,JVM内存结构是内存区域的物理划分,两者完全不同的概念
  • 可见性问题的硬件根因是CPU私有缓存,JMM用"本地内存"抽象了它
  • volatile通过内存屏障强制刷新写缓冲区、使其他核心缓存失效,保证可见性
  • volatile通过禁止特定位置的重排序保证有序性,但不禁止所有重排
  • happens-before是判断并发操作间可见性的唯一标准,掌握八条规则即可
  • DCL单例必须加volatile,因为new操作可能被重排,导致拿到半初始化对象
  • volatile不保证原子性,i++这种复合操作需要synchronized或AtomicInteger
  • JMM的设计目的是屏蔽不同硬件平台的差异,提供统一的内存可见性保证

下一篇预告:第2篇——synchronized与ReentrantLock深度对比。

http://www.jsqmd.com/news/740163/

相关文章:

  • 如何5分钟解锁中兴光猫完整权限:zteOnu工具终极指南
  • 2026年3月艺术疗愈课程推荐,青少年一对一心理咨询/青少年心理咨询/心理咨询/一对一心理咨询,艺术疗愈机构口碑推荐 - 品牌推荐师
  • MySQL多表联查时,Column ‘xxx‘ is ambiguous 报错?别慌,3分钟教你彻底搞懂并解决它
  • IoTAutomationFramework_2.0 安卓测试自动化工具
  • 深入对比:Zynq上AXI UARTLite vs UART 16550,多路串口方案到底怎么选?
  • 2026年最新排名:中国质量协会六西格玛考试含金量怎么样(附避坑榜) - 众智商学院课程中心
  • [C# 笔记] 如何设置消息钩子 (以低级鼠标钩子为例)
  • 为什么选择优德营造Omakase设计打造你的日料餐厅?
  • 原神游戏数据采集与分析实战指南
  • 2026 选什么降 AI 软件不踩坑?看排行前先搞懂这 3 个降 AI 平台差异。 - 我要发一区
  • GSPO算法:序列级策略优化在旅行规划中的应用
  • **2026年5月六西格玛认证排行榜|黑带VS绿带含金量与报考评价** - 众智商学院课程中心
  • Linux系统PPP拨号全攻略:从串口调试到断线自动重连的完整实现
  • 04 接雨水 单调栈
  • Ultralytics LLM:将YOLO工程哲学带入大语言模型应用开发
  • 开源桌面示波器Haasoscope:FPGA+MCU架构与Python客户端全解析
  • 深度解析applera1n:基于checkm8漏洞的iOS激活锁绕过技术实现
  • 中山AI优化提供商哪家强?原来有这些选择!
  • OBS虚拟摄像头进阶玩法:除了共享屏幕,还能在腾讯会议里玩出什么花?
  • 毕业答辩前选哪款降 AI 软件?2026 排行前 5 让 AI 率降到 5% 以下! - 我要发一区
  • 第二章、application.properties文件的配置
  • 2026年5月六西格玛绿带黑带含金量排行|报考避坑榜Top5 - 众智商学院课程中心
  • Ubuntu Server 24.04下解决SunloginClient 向日葵依赖libgconf-2-4安装问题
  • SAP SD新手避坑:VA01创建销售订单报‘无定价过程’?手把手教你用OVKK搞定配置
  • 从Pikachu靶场看企业级Web安全:这些漏洞在真实业务中如何防御?
  • MAA明日方舟自动化助手完整指南:如何一键解放双手高效长草
  • 论文 AI 率从 78% 降到 3.2%!2026 排行前 3 降 AI 软件让你赶上答辩。 - 我要发一区
  • ESXi 7.0U3迁移实战:手把手教你用命令行把旧主机配置‘克隆’到新服务器
  • 告别串口助手!手把手教你用TC264打造一个“硬件版”参数配置器
  • 【读书笔记】《你就是孩子最好的玩具》