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

举报
喵手 发表于 2026/01/15 16:54:06 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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 = new Singleton(); // 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(有数据竞争 / 不安全)

private static boolean running = true;

Thread t = new Thread(() -> {
  while (running) { /* busy-wait */ }
});
t.start();

Thread.sleep(100);
running = false; // updater

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

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

private static volatile boolean running = true;

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

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

private static volatile int counter = 0;
counter++; // 在两个线程中并发执行

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

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

class Data { final int v; Data(int v){ this.v = v; } }

Data d = new Data(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 的情况下运行大量次,统计 r1==0 && r2==0 出现次数(无同步时可能出现;加 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 !!!


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

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


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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。