Java 内存模型(JMM)——深入happens-before、可见性、volatile、synchronized 与 final 语义!

Java 内存模型(JMM)——深入happens-before、可见性、volatile、synchronized 与 final 语义!
㊗️本期内容已收录至专栏《Java核心实操(进阶版)》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期难度指数:⭐⭐⭐
🉐福利:一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

1. happens-before 规则集合(核心一览)

happens-before 是 JMM 的核心:如果动作 A happens-before 动作 B,则 A 的结果对 B 可见,且 A 不会被重排序到 B 之后。它是一个传递关系(若 A hb B 且 B hb C,则 A hb C)。

常见规则(你必须记住):

  1. 程序次序规则(Program order):同一线程内,按程序顺序的操作 A hb 后面操作 B。
  2. Monitor(锁)规则:对同一 monitor,unlock(h) happens-before subsequent lock(h)。也就是说,释放锁的操作对随后获得同一把锁的线程可见。
  3. volatile 规则:对 volatile 变量的写 happens-before 该变量后续的读(写 -> 读)。
  4. 线程启动/终止
    • Thread.start():在启动线程前发生的操作 happens-before 新线程的运行开始。
    • Thread.join():被 join 的线程中的操作 happens-before join 返回之后的操作。
  5. 传递性:若 A hb B 且 B hb C,则 A hb C。
  6. 中断(Interrupt):在调用 t.interrupt() 的线程中,interrupt 发生之前的操作 hb 之后在被中断线程中对 interrupt 状态的观察;(interrupt 的具体规则较细,按需查 JLS)。
  7. final 字段:构造器结束且 this 未逸出时,对 final 字段的写对其他线程是可见的(有特殊的发布保证,详见第 3 节)。
关键结论:只要写操作没有通过某种 hb 关系“连到”读操作,JMM 就允许可见性失效与重排序产生的行为。判断安全性就是判断是否存在合适的 hb 路径。

2. volatile 的语义:acquire / release 与内存屏障(直观理解)

在 Java 里,volatile 并不是“只是可见性”,它在 JMM 层面提供了一种轻量级的同步

  • volatile 变量的 提供 release 语义(写之前的所有内存写在逻辑上对其他线程可见,不会被重排到 volatile 写之后)。
  • volatile 变量的 提供 acquire 语义(读之后的内存读/写不会被重排到 volatile 读之前;并且可以看到之前对其它变量的写,只要这些写 happens-before 了那个 volatile 写)。
  • 在 JMM 层面:volatile write hb volatile read。因此 volatile 可用于消息传递(signal)与禁止特定类型的重排序。
实用口訣:volatile write = release(像“发布”),volatile read = acquire(像“获取”)。配合使用可保证写入方的变化对读取方可见,并阻止某些指令重排序。

内存屏障(概念,不强求汇编细节)

实现上 JVM/CPU 会插入内存屏障(fence)或利用强内存序指令来实现 volatile 的 acquire/release。不同架构(x86/ARM)与 JVM 实现会有差异,但高层语义如上保持不变。

我可以在下一步给出 HotSpot 在 x86/ARM 上常见的屏障映射及简化汇编示例(如果你想要具体实现细节,请回复“给我屏障实现”)。

3. 构造函数与 final 字段的发布保证(非常重要且易被误解)

JMM 对 final 字段提供额外的初始化安全保证

  • 若一个 final 字段在构造器中被初始化,并且在构造器完成之前 this 没有逸出(即没有把对象引用泄露到其他线程),那么其他线程在看到该对象引用时,一定能看到 final 字段的正确构造值,即使没有同步措施也能安全读取。
  • 该保证不适用于非 final 字段。对非-final 字段,必须使用 hb(volatile/锁/其他安全发布方式)来保证可见性。
  • 若构造过程中 this 逸出(例如把 this 放到某个全局静态集合或启动线程并访问),则 final 保证被破坏。

安全发布对象的常见方式(会建立 hb):

  • 将对象引用写入 volatile 变量;
  • synchronized 块内发布(写入/读出时使用同一把锁);
  • 通过 ConcurrentHashMapBlockingQueue 等并发集合发布;
  • 在静态初始化器中发布(类初始化是有同步语义的);
  • 发布前后使用 Thread.start() / Thread.join() 等。

4. 重排序示例与分析(经典案例与解释)

下面几个经典并发例子说明 JMM 允许的/不允许的行为,以及如何用 hb 规则分析。

示例 1:消息传递(无同步)

// Thread 1 a =1;// W1 b =1;// W2// Thread 2 r1 = b;// R3 r2 = a;// R4

如果没有任何同步(volatile/锁等),JMM 允许 r1 == 0 && r2 == 0 出现吗?是允许的。原因:写操作在不同线程间没有 happens-before 联系,且编译器/JIT/CPU 可重排序或缓存导致读取线程看到旧值。

示例 2:双重检查(DCL)——错误与修复

错误版本(非 volatile):

if(instance ==null){synchronized(...){if(instance ==null){ instance =newSingleton();// allocation, init, assign — 可能重排序}}}

问题:instance = new Singleton() 在底层可能被重排序为(allocate -> assign -> init),导致另一个线程看到非 null 的 instance 但对象仍未初始化完,出现奇怪行为。解决:将 instance 声明为 volatile,使得写的 release 与随后的读的 acquire 建立必要的内存屏障,从而禁止该关键重排序的可见性。

示例 3:经典 2-线程重排序三次写读(允许同时为 0)

// T1 x =1; r1 = y;// T2 y =1; r2 = x;

没有同步时,JMM 允许 r1 == 0 && r2 == 0,因为两边的写可能对各自线程不可见或被重排序。加 volatile(把 x 或 y 标为 volatile 并形成适当 hb)或使用锁可以禁止该情况。

5. 实战练习:多个并发场景(每个标注是否有数据竞争 / 是否安全发布)

先定义:数据竞争(data race) = 两个线程对同一变量进行访问(至少有一次写),且这些访问没有通过 happens-before 关系进行同步 -> JMM 允许不可定义行为(可见性/重排序问题)。

场景 A — 非 volatile flag 的 busy-wait(有数据竞争 / 不安全)

privatestaticboolean running =true;Thread t =newThread(()->{while(running){/* busy-wait */}}); t.start();Thread.sleep(100); running =false;// updater

分析running 的写与循环中的读之间没有 hb,可能出现循环永远不结束(读取线程看不到更新)。结论:有数据竞争。改为 volatile 即可安全发布。

场景 B — volatile flag 的 busy-wait(安全发布)

privatestaticvolatileboolean running =true;

分析:写 running = false hb 后续读到 volatile 的线程,循环会结束。结论:安全(可见性成立)。

场景 C — counter++ with volatile(非原子)

privatestaticvolatileint counter =0; counter++;// 在两个线程中并发执行

分析volatile 只保证每次读/写的可见性,但 counter++ 包含读—改—写三步,不是原子。因此存在数据竞争(丢失更新)。结论:不安全;改用 AtomicIntegersynchronized

场景 D — 发布不可变对象(使用 final 字段,直接发布引用到普通容器)

classData{finalint v;Data(int v){this.v = v;}}Data d =newData(42); sharedContainer.add(d);// sharedContainer 非并发(无同步)

分析:如果构造器完成且 this 没有在构造期间逸出,那么任何线程取得 d 引用时,v 的值对它是可见(final 保证)。结论:对于 final 字段,是安全的;但前提是构造期间没有逸出。

场景 E — DCL without volatile(可能失败)

已在第 4 节说明:有数据竞争 / 不安全。加 volatile 修复。

场景 F — 发布通过 ConcurrentHashMap(安全)

ConcurrentHashMap.put("k", obj);

分析:ConcurrentHashMap 的 put 有必要的同步/内存语义,能安全发布对象引用。结论:安全发布。

6. 常见陷阱(务必牢记)

  • volatile 当锁volatile 不保证复合操作的原子性;不能替代锁来保护复合读写或多个变量的一致性。
  • 以为 synchronized 很慢:现代 JVM 优化很多(biased lock、lock elision 等),在很多情形下 synchronized 性能足够好且正确性明确。先正确再优化。
  • 误用 final:构造期间逸出会破坏 final 保证
  • 忽略转移性与传递路径:有时用多个同步手段组合创造 hb 路径(transitive hb),务必清晰思考每一步的 hb。
  • 以为 volatile 禁止所有重排序volatile 禁止的只是与 volatile 读/写相关的某些重排序,并非完全禁止所有重排序。

7. 排查并发/可见性问题的实战清单

  1. 写最小复现:把问题缩到最小可运行示例(往往能暴露问题根源)。
  2. 标注所有共享变量与访问点:哪些是写,哪些是读,是否存在同步(volatile/锁/atomic)。
  3. 检查 happens-before 链:对于每个读,能否找到写到读的 hb 路径?没有则存在数据竞争。
  4. 使用简单修复验证假设:把可疑变量改为 volatile 或加锁,或用 Atomic,看问题是否消失(这是诊断手段)。
  5. 使用工具jstack、Java Flight Recorder、async-profiler、hs-err 日志、ThreadMXBean 等查看死锁、阻塞、上下文切换与热点。
  6. 代码审计:查找 this 在构造器内部逸出的代码、全局集合写入点、静态初始化器等危险用法。
  7. 设计改进:优先使用不可变对象、消息传递(队列)、并发集合与高层并发构件(Executor、CompletableFuture)。

8. 建议的实战练习(可直接复现)

  1. 实验 1(可见性):运行示例 A(非 volatile flag)与改为 volatile 的版本,观察线程是否能终止。
  2. 实验 2(原子性):并发两个线程对 volatile int countercounter++,观察结果,再改为 AtomicInteger 比较。
  3. 实验 3(DCL):构建一个 heavy init 的 Singleton,测不加 volatile 时是否出现半初始化错误(可能需要在多核/不同 JVM 下触发),再加 volatile 修复。
  4. 实验 4(final 发布):构造含 final 字段的对象并在构造过程中测试 “this 逸出” 的情况,验证 final 保证是否失效。
  5. 实验 5(重排序观察):写经典的两线程写/读 x,y 的示例(T1: x=1; r1=y; T2: y=1; r2=x;)在无同步/加 volatile 的情况下运行大量次,统计 r10 && r20 出现次数(无同步时可能出现;加 volatile 后不应出现)。

我可以把这些示例都写成可直接运行的 Java 程序,或打包成 Maven 项目给你下载 —— 你想要吗?

9. 延伸阅读(权威资源)

  • Java Language Specification(JLS)第 17 章:Java 内存模型(权威规范)。
  • Java Concurrency in Practice(Brian Goetz 等)—— 并发实战经典。
  • “The Java Memory Model”(Manson, Pugh 等论文)—— 设计 rationale。
  • HotSpot 源码 / OpenJDK 文档 —— 想看具体实现(内存屏障、汇编插桩、JIT 优化)。

10. 总结(一句话)

并发问题归根结底是三件事:可见性(visibility)有序性(ordering)原子性(atomicity)。判断一个并发方案是否正确,就是在这三者之间找到合适的折衷并用 JMM 的 happens-before 关系证明每个读能“看到”它应看到的写。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。

版权声明:本文由作者原创,转载请注明出处,谢谢支持!

Read more

《算法题讲解指南:优选算法-滑动窗口》--13 水果成篮

《算法题讲解指南:优选算法-滑动窗口》--13 水果成篮

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 13 水果成篮 题目链接: 编辑 题目示例: 解法(滑动窗口): 算法思路: 算法流程: C++代码演示:方法一(使用容器) C++代码演示:方法二(用数组模拟哈希表) 算法总结及流程解析: 结束语 13 水果成篮 题目链接: 题目示例: 解法(滑动窗口): 算法思路:       研究的对象是一段连续的区间,可以使用【滑动窗口】思想来解决问题。       让滑动窗口满足:窗口内水果的种类只有两种。       做法:右端水果进入窗口的时候,

By Ne0inhk
当AI变成“需求读心术大师“:Python开发者如何用“脑洞算法“破解预测困局?

当AI变成“需求读心术大师“:Python开发者如何用“脑洞算法“破解预测困局?

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎点赞 + 收藏 + 关注哦 💕 当AI变成"需求读心术大师":Python开发者如何用"脑洞算法"破解预测困局? 📚 本文简介 本文探讨了AI需求预测的局限性及其与人类心理洞察的本质差异。通过Python代码示例(GradientBoostingClassifier模型)揭示了AI"读心术"实为基于历史数据的概率猜测,并运用mermaid图对比展示AI在情感理解、文化背景考量等方面的不足。关键发现: AI预测依赖表面行为数据,而人类能理解深层动机 开发者应结合算法与人文洞察,如文中小陈从"更快的马"解读出"便捷交通工具"的真实需求 提出Python开发场景对照表,显示人类在用户体验设计、错误处理等方面的温度优势 结论:AI预测是工具而非真理,开发者需保持批判思维,

By Ne0inhk

悬架&天棚算法

悬架 * 悬架的定义: * 连接车轮与车身的**机构。支撑车身保持几何姿态+**缓冲路面冲击+传递车轮与路面间的力和力矩,保证轮胎抓地力,关乎操控稳定与安全 * 悬架设计的难点:“舒适性”与“操控性”的权衡 * 舒适性:需要“软”悬架,隔离路面振动 * 操控性:需要“硬”悬架,减少车身侧倾、俯仰,保持轮胎紧贴路面 * 注释:软悬架(刚度低)软悬架能够更好地吸收高频振动,如路面接缝或不平引起的冲击。它通过降低共振频率,减少车身振动对乘客的影响,从而提升乘坐舒适性。然而,过于柔软的悬架可能导致低频振动(如长波路面)的持续时间更长,可能引起乘客的不适感。 * 注释:硬悬架能够更快地抑制振动,减少车身的晃动,但在处理高频振动时可能不够柔和,导致更多的振动传递到车内,影响舒适性。 * 软悬架的共振频率较低,能够更好地适应高频振动,减少乘客感受到的振动强度。而硬悬架的共振频率较高,可能与人体敏感的频率范围(

By Ne0inhk
【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现

【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人等方向学习者 ❄️个人专栏:《C语言》《【初阶】数据结构与算法》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、带环链表 * 1.1题目 * 1.2 算法原理 * 1.3 代码 * 1.4 数学证明 * 1.4.1 为什么带环slow与fast必定能相遇? * 1.4.2 fast一定只能走2步吗?可以是2步甚至更多吗? * 1.4.2.1 以3步为例 * 1.4.3结论 * 二、环形链表(寻找相遇点) * 2.1 题目

By Ne0inhk