news 2026/9/24 3:05:41

深入 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/9/23 22:14:39

GTE多语言文本嵌入实战:跨境电商商品搜索优化方案

GTE多语言文本嵌入实战:跨境电商商品搜索优化方案 1. 跨境电商搜索的痛点,我们每天都在经历 你有没有在跨境电商平台上搜过“wireless earbuds”?结果页面里跳出一堆完全不相关的商品——可能是有线耳机、蓝牙音箱,甚至还有耳机…

作者头像 李华
网站建设 2026/9/23 4:17:21

Qwen2-VL-2B-Instruct开源镜像部署教程:3步搭建本地多模态检索系统

Qwen2-VL-2B-Instruct开源镜像部署教程:3步搭建本地多模态检索系统 你是不是经常遇到这样的烦恼?电脑里存了几千张照片,想找一张“带猫的沙发照片”,却只能一张张翻看;或者想从一堆产品描述里,快速找到和某…

作者头像 李华
网站建设 2026/9/22 8:01:29

GLM-4-9B模型蒸馏实战:小模型性能提升秘籍

GLM-4-9B模型蒸馏实战:小模型性能提升秘籍 最近在折腾大模型部署的时候,经常遇到一个头疼的问题:模型太大,显存不够用。特别是像GLM-4-9B这样的模型,虽然性能不错,但动辄需要几十GB的显存,普通…

作者头像 李华
网站建设 2026/9/22 1:40:05

TranslateGemma-27B性能优化:利用GPU加速实现毫秒级翻译响应

TranslateGemma-27B性能优化:利用GPU加速实现毫秒级翻译响应 翻译任务对响应速度的要求有多高?想象一下,你在浏览一个外文网站,或者与海外客户实时沟通,每多等一秒钟,体验就会大打折扣。传统的翻译服务要么…

作者头像 李华
网站建设 2026/9/20 18:13:03

突破限制:百度网盘直链提取技术解析

突破限制:百度网盘直链提取技术解析 【免费下载链接】baidu-wangpan-parse 获取百度网盘分享文件的下载地址 项目地址: https://gitcode.com/gh_mirrors/ba/baidu-wangpan-parse 本文介绍一种能够绕过百度网盘限速机制的技术方案,通过解析分享链接…

作者头像 李华
网站建设 2026/9/21 21:15:45

OFA-COCO蒸馏版入门指南:理解distillation loss设计与COCO caption分布拟合

OFA-COCO蒸馏版入门指南:理解distillation loss设计与COCO caption分布拟合 提示:本文包含约3800字内容,预计阅读时间12分钟。通过生活化类比和实际案例,帮助初学者理解知识蒸馏和图像描述技术。 1. 引言:让AI学会&quo…

作者头像 李华