【JUC】JMM(Java 内存模型)

深入浅出 Java 内存模型(JMM)

JMM(Java Memory Model)是 Java 并发编程的基石,但它常常让人感到抽象难懂。本文用"人话+例子"帮你彻底搞懂。


一、JMM 到底在解决什么问题?

先想一个场景:

java 复制代码
// 线程 A
flag = true;

// 线程 B
while (!flag) { }

问题来了:

  • 线程 A 改了 flag,线程 B 一定能马上看到吗?
  • 编译器会不会把 while(!flag) 优化成死循环?
  • CPU 缓存会不会导致两个线程读到不同的值?

JMM 就是 Java 官方给出的"标准答案" :它定义了多线程之间共享变量的可见性、有序性和原子性规则,屏蔽了不同硬件/操作系统的差异,让 Java 程序在任何平台上都有一致的并发行为。

一句话总结:JMM 规定了线程之间如何通过内存进行通信,以及什么时候一个线程的写操作对另一个线程可见。


二、先理解"抽象内存模型"

JMM 抽象出两层结构:

复制代码
┌─────────────┐         ┌─────────────┐
│  线程 A     │         │  线程 B     │
│ 工作内存    │         │ 工作内存    │
│ (副本)      │         │ (副本)      │
└──────┬──────┘         └──────┬──────┘
       │  read/write           │
       ▼                       ▼
┌─────────────────────────────────────┐
│           主内存(共享变量)          │
└─────────────────────────────────────┘
  • 主内存:所有共享变量存在这里
  • 工作内存:每个线程有自己的副本(对应 CPU 缓存/寄存器)

线程读写变量,实际是:从主内存拷贝到工作内存 → 修改 → 再写回主内存。这就产生了可见性问题------A 改了主内存,B 的工作内存可能还是旧值。

注意:工作内存是抽象概念,不是真的内存块,它对应 CPU 寄存器、缓存等。


三、三大核心特性

1. 原子性(Atomicity)

一个操作不可分割,要么全做,要么不做。

问题例子:

java 复制代码
int count = 0;
count++;  // 实际是三步:读 → 加1 → 写回,不是原子的!

多线程下 count++ 会丢更新。解决:synchronized、AtomicInteger、Lock。

JMM 只保证基本类型(除 long/double)的读写 是原子的,i++ 这种复合操作不保证。

2. 可见性(Visibility)

一个线程修改共享变量后,其他线程能立刻看到。

问题例子就是开头的 flag。解决:

  • volatile:强制读写主内存,保证可见性
  • synchronized / Lock:释放锁前刷回主内存
3. 有序性(Ordering)

程序执行的顺序按照代码先后顺序执行------但编译器和 CPU 会为了性能做指令重排序,单线程下没问题(保证最终结果一致),多线程下可能出 bug。

经典例子(双重检查锁单例):

java 复制代码
public class Singleton {
    private static volatile Singleton instance;  // volatile 关键!
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();  // 这行不是原子的!
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 实际分三步:

  1. 分配内存
  2. 初始化对象
  3. 把引用指向内存

如果重排序成 1→3→2,另一个线程可能拿到还没初始化完的对象 。加 volatile 禁止这种重排。


四、Happens-Before 原则(核心!)

JMM 用 happens-before 来定义可见性:如果 A happens-before B,那么 A 的结果对 B 可见。

记住这几条就够了:

规则 含义
程序顺序规则 同一线程内,前面的操作 hb 后面的操作
锁规则 解锁 hb 后续对同一锁的加锁
volatile 规则 对 volatile 变量的写 hb 后续对该变量的读
线程启动规则 Thread.start() hb 该线程的所有操作
线程终止规则 线程所有操作 hb 其他线程检测到它终止(join)
传递性 A hb B,B hb C ⟹ A hb C

举例理解:

java 复制代码
int x = 1;              // ①
volatile boolean flag = true;  // ② 写 volatile

// 另一线程
if (flag) {             // ③ 读 volatile
    System.out.println(x);  // ④ 一定输出 1
}

因为 ① hb ②(程序顺序),② hb ③(volatile 规则),③ hb ④(程序顺序),传递得 ① hb ④,所以 x=1 可见。

happens-before 的本质:它不要求真的"先执行",只要求前者的结果对后者可见。 这给了编译器和 CPU 重排的空间,只要不违反 hb 规则就行。


五、volatile 深入

volatile 是 JMM 最轻量的同步手段,它保证两件事:

  1. 可见性:写 volatile 变量立刻刷回主内存,读时从主内存读
  2. 有序性:禁止指令重排序(通过内存屏障 Memory Barrier 实现)

它不保证原子性! volatile int i; i++; 依然不安全。

底层原理:volatile 写前后插入内存屏障,禁止屏障两侧的指令重排,同时触发缓存一致性协议(如 x86 的 MESI)。

适用场景:状态标志位、DCL 单例、一写多读的场景。


六、总结一张图

复制代码
         JMM 三大特性
    ┌────────┼────────┐
  原子性    可见性    有序性
    │         │         │
synchronized volatile  volatile
  Lock      锁        happens-before
  Atomic               内存屏障

学习建议:

  1. 先理解"工作内存/主内存"抽象
  2. 记住三大特性各自的问题和解法
  3. 吃透 happens-before 六条规则
  4. 用 DCL 单例和 flag 循环两个例子反复推演

JMM 的精髓在于:它允许重排序和缓存来提升性能,但通过 happens-before 规则保证程序员需要的那部分正确性。理解了这一点,并发编程的很多"坑"就豁然开朗了。

相关推荐
十碗饭吃不饱4 小时前
MySQL InnoDB RR 隔离下二级索引临键锁引发 INSERT 锁超时 全链路解析
java·数据库·mysql
专业程序开发源4 小时前
springboot校企合作双选会一站式预约与面试系统16010-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·面试·php·课程设计
Elastic 中国社区官方博客5 小时前
Observability:当 YAML 遇上 IConfiguration:OpenTelemetry .NET SDK 的声明式配置
大数据·开发语言·elasticsearch·搜索引擎·全文检索·.net
郑州光合科技余经理5 小时前
同城商城源码:商品库和订单库怎么随包交付
android·java·开发语言·后端·uni-app·php·ai编程
\光辉岁月/5 小时前
4.String类
java·面向对象·stringbuilder·string·stringbuffer·字符串类
welfare9625 小时前
新号别搞:C++_list
开发语言·c++
数据狐(Datafox)5 小时前
一文读懂京东商品详情 API:返回字段解读与 JSON 样例
开发语言·数据库·爬虫·json
蚰蜒螟5 小时前
从 Java SocketChannel 到 TCP 挥手:一次 close() 的完整调用链与引用计数解析
java·python·tcp/ip