news 2026/4/23 17:37:05

深入 Java 内存模型(JMM):Happens-Before、volatile 与 DCL 单例陷阱详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 Java 内存模型(JMM):Happens-Before、volatile 与 DCL 单例陷阱详解

在多线程编程中,你是否遇到过这样的问题:

  • 线程 A 修改了变量,线程 B 却“看不见”?
  • 程序逻辑看似正确,却偶尔出现NullPointerException
  • 双重检查锁定(DCL)单例在高并发下竟然返回了未初始化的对象?

这些问题的根源,都指向Java 内存模型(JMM)。本文将用通俗语言 + 代码实战 + 反例剖析,带你彻底搞懂 JMM 的核心机制,尤其是Happens-Before 规则volatile 关键字的作用,并揭秘DCL 单例为何必须加 volatile


一、为什么需要 Java 内存模型(JMM)?

🧩 硬件现实 vs Java 理想

现代 CPU 为了提升性能,会做两件事:

  1. 每个 CPU 核心有自己的高速缓存(Cache)
  2. 编译器和处理器会重排序指令

这导致:线程对共享变量的修改,其他线程可能看不到!

💡 JMM 的目标:
屏蔽底层硬件差异,为程序员提供一套清晰的内存可见性规则


二、JMM 核心:主内存 vs 工作内存

  • 主内存(Main Memory):所有线程共享,存储实例字段、静态字段等。
  • 工作内存(Working Memory):每个线程私有,保存主内存变量的副本。
线程A ──[读/写]──→ 工作内存A ──[load/store]──→ 主内存 线程B ──[读/写]──→ 工作内存B ──[load/store]──→ 主内存

⚠️关键问题:线程 A 修改了工作内存中的变量,不会立即同步到主内存,线程 B 也无法自动感知!


三、Happens-Before 规则:JMM 的“法律条文”

Happens-Before(先行发生)是 JMM 定义的可见性保证规则。如果操作 A happens-before 操作 B,那么 A 的结果对 B一定可见

✅ 8 大 Happens-Before 规则(重点掌握前5条)

规则说明示例
1. 程序顺序规则同一线程内,前面的操作 hb 后面的操作a=1; b=2;→ a=1 hb b=2
2. 监视器锁规则unlock hb 后续对同一锁的 locksynchronized 块之间
3. volatile 变量规则对 volatile 的写 hb 后续对该变量的读volatile flag;写后读可见
4. 线程启动规则Thread.start() hb 线程内任意操作主线程 start() 后子线程可见其之前变量
5. 线程终止规则线程内所有操作 hb 其他线程检测到终止(如 join() 返回)t.join() 后可看到 t 中所有修改
6. 中断规则interrupt() hb 被中断线程检测到中断
7. final 域规则构造函数中 final 字段的写 hb finalize()
8. 传递性A hb B, B hb C ⇒ A hb C

🔑核心思想:只要两个操作存在 hb 关系,JMM 就保证可见性;否则,不保证!


四、volatile 关键字:可见性 + 禁止重排序

✅ volatile 的两大作用

1.保证可见性
  • 写 volatile 变量时,强制刷新工作内存到主内存
  • 读 volatile 变量时,强制从主内存加载最新值
public class VolatileDemo { private volatile boolean running = true; public void stop() { running = false; // 其他线程立即可见! } public void run() { while (running) { // 每次都从主内存读取 // do something } } }

❌ 若不用 volatile,running = false可能只写入工作内存,while 循环永远停不下来!

2.禁止指令重排序
  • 编译器/CPU 不会对 volatile 读写进行重排序
  • 插入内存屏障(Memory Barrier)阻止重排
// 伪代码:对象初始化过程 instance = new Singleton(); // 实际分三步: // 1. 分配内存 // 2. 初始化对象 // 3. instance 引用指向内存地址 // 若 2 和 3 重排序 → 先赋值引用,再初始化!

volatile 能禁止这种危险重排!


五、双重检查锁定(DCL)单例:为什么必须加 volatile?

❌ 错误写法(无 volatile)

public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 危险! } } } return instance; } }

⚠️ 问题:指令重排序导致“半初始化”对象

instance = new Singleton()在 JVM 中分为三步:

  1. memory = allocate();// 分配内存
  2. ctorInstance(memory);// 调用构造函数初始化
  3. instance = memory;// 引用指向内存

若步骤 2 和 3 重排序→ 先执行 3,再执行 2!

🧨 后果:
线程 A 执行到第 3 步(instance != null),但对象尚未初始化;
线程 B 此时调用getInstance(),拿到一个未初始化的 instance→ 使用时抛出NullPointerException

✅ 正确写法:加 volatile

public class Singleton { private static volatile Singleton instance; // 关键! public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

volatile 的作用

  1. 禁止new Singleton()的指令重排序(保证初始化完成后再赋值)
  2. 保证其他线程能看到完全初始化后的对象(可见性)

六、反例警示:这些 volatile 误用很常见!

❌ 反例1:volatile 不能保证原子性

private volatile int count = 0; public void increment() { count++; // 实际是 read + inc + write,非原子! }

→ 多线程下仍可能丢失更新!
✅ 正确做法:用AtomicIntegersynchronized


❌ 反例2:依赖 volatile 实现“伪同步”

volatile boolean flag = false; // 线程A flag = true; data = "hello"; // 非 volatile 变量 // 线程B if (flag) { System.out.println(data); // 可能打印 null! }

只有 flag 有 hb 关系,data 没有!
✅ 正确做法:data 也用 volatile,或用锁保护整个操作。


七、总结:JMM 核心要点速记

概念关键点
JMM 目标屏蔽硬件差异,定义多线程内存可见性规则
Happens-Before是可见性的“法律依据”,无 hb 关系则无可见性保证
volatile 作用1. 可见性 2. 禁止重排序(不保证原子性
DCL + volatile防止对象“半初始化”,确保安全发布

🚀一句话口诀
“hb 定可见,volatile 防重排,DCL 不加它,空指针等着你!”


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!(发点评论可以给博主加热度哦)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 11:37:02

Qwen3-Reranker-0.6B在QT图形界面中的集成开发教程

Qwen3-Reranker-0.6B在QT图形界面中的集成开发教程 你是不是也遇到过这样的场景:手里有一个很棒的AI模型,比如阿里刚开源的Qwen3-Reranker-0.6B,想把它做成一个桌面应用,让不懂代码的同事也能轻松用上?或者你想给自己…

作者头像 李华
网站建设 2026/4/23 12:58:28

深度学习中的LaTeX应用:论文写作与公式编辑

深度学习中的LaTeX应用:论文写作与公式编辑 1. 为什么深度学习研究者离不开LaTeX 在实验室的深夜,当模型训练完成、实验数据整理妥当,真正让人头疼的往往不是代码调试,而是如何把那些复杂的数学推导和严谨的实验分析清晰地呈现出…

作者头像 李华
网站建设 2026/4/23 13:18:12

Retinaface+CurricularFace镜像体验:一键搭建人脸比对系统

RetinafaceCurricularFace镜像体验:一键搭建人脸比对系统 你是不是也遇到过这样的场景?公司需要做一个简单的员工考勤系统,或者自己有个小项目想加入人脸验证功能。一查资料,发现人脸识别技术听起来高大上,但真要动手…

作者头像 李华
网站建设 2026/4/23 16:28:50

Qwen2-VL-2B-Instruct效果展示:植物标本图与科属特征描述匹配TOP案例

Qwen2-VL-2B-Instruct效果展示:植物标本图与科属特征描述匹配TOP案例 1. 引言:当AI能“看懂”植物 想象一下,你是一位植物学爱好者,在野外拍到了一株不知名的植物。你翻遍图鉴,对着照片和文字描述反复比对&#xff0…

作者头像 李华
网站建设 2026/4/23 14:20:15

丹青识画保姆级教程:3步完成OFA多模态模型本地部署

丹青识画保姆级教程:3步完成OFA多模态模型本地部署 1. 学习目标与价值 想让自己开发的AI应用能够看懂图片内容,并用优美的中文描述出来吗?今天介绍的丹青识画系统,基于OFA多模态模型,能够将普通图片转化为充满诗意的…

作者头像 李华