Java 内存模型(JMM)通俗解释

1. JMM 是什么

JMM 全称是 Java Memory Model ,中文叫 Java 内存模型

它不是在讲某一块真实硬件内存,也不是直接在讲 CPU 的 L1、L2、L3 缓存。

它是一套 Java 多线程规范,用来规定:

text 复制代码
1. 一个线程修改变量后,其他线程什么时候能看到?
2. 编译器和 CPU 能不能调整代码执行顺序?
3. 哪些操作天然安全,哪些操作需要 volatile、锁或 Atomic 类?

一句话:

JMM 是 Java 对多线程读写共享变量的一套抽象规则。


2. 为什么需要 JMM

Java 程序运行时会有很多优化:

  • 编译器可能重排序指令;
  • JIT 可能把变量缓存到寄存器;
  • CPU 会用 L1、L2、L3 缓存;
  • CPU 也可能乱序执行;
  • 一个线程改了变量,另一个线程不一定马上看到。

例如:

java 复制代码
class Demo {
    private boolean running = true;

    public void loop() {
        while (running) {
            // do something
        }
    }

    public void stop() {
        running = false;
    }
}

你以为一个线程调用 stop() 后,另一个线程里的 loop() 会退出。

但如果 running 不是 volatile,循环线程可能一直读到旧值 true,导致停不下来。

这就是可见性问题。


3. JMM 的两个抽象概念

JMM 抽象出两个概念:

text 复制代码
主内存 Main Memory
工作内存 Working Memory

可以画成这样:

text 复制代码
                 主内存
        所有线程共享变量的抽象存储区
              /             \
             /               \
       工作内存 A           工作内存 B
       Thread A             Thread B

线程操作共享变量时,抽象流程是:

text 复制代码
1. 从主内存读取变量
2. 拷贝到自己的工作内存
3. 在线程中使用、修改
4. 再写回主内存

注意:

主内存和工作内存都是 JMM 的抽象,不是硬件里固定的某一块内存。


4. 主内存是不是物理内存 RAM?

不是完全等于。

可以粗略理解成:

text 复制代码
主内存 ≈ Java 中所有线程共享变量的最终存储位置

但 JMM 不关心变量实际在 RAM、CPU Cache 还是寄存器里。

JMM 只关心:

text 复制代码
线程 A 写了变量,线程 B 什么时候能看到?

5. 工作内存是不是 L1、L2、L3?

不是。

工作内存是 Java 层面的抽象,不等于某一级 CPU 缓存。

它底层可能对应:

text 复制代码
CPU 寄存器
L1 Cache
L2 Cache
L3 Cache
Store Buffer 写缓冲
Load Buffer 读缓冲
JIT 优化出来的临时副本
线程栈上的局部副本

所以不能说:

text 复制代码
工作内存 = L1
工作内存 = L2
工作内存 = L3

更准确地说:

工作内存代表线程可能持有的共享变量副本,底层可能落在寄存器、CPU 缓存、写缓冲区等地方。


6. CPU 缓存大概是什么

硬件上通常有:

text 复制代码
寄存器:最快,容量最小
L1 Cache:通常每个 CPU 核心私有
L2 Cache:通常每个 CPU 核心私有,容量比 L1 大
L3 Cache:通常多个核心共享
RAM:物理内存,最慢

大致访问层级:

text 复制代码
寄存器 -> L1 -> L2 -> L3 -> RAM

但 JMM 不直接规定使用哪一级缓存。


7. volatile 是什么

volatile 是 Java 的关键字,主要保证两件事:

text 复制代码
1. 可见性
2. 禁止特定重排序

示例:

java 复制代码
class Demo {
    private volatile boolean running = true;

    public void loop() {
        while (running) {
            // do something
        }
    }

    public void stop() {
        running = false;
    }
}

加了 volatile 后,一个线程执行:

java 复制代码
running = false;

另一个线程就能看到这个变化。


8. volatile 是不是让线程不用缓存?

不是。

这个说法不准确:

text 复制代码
volatile 让线程不用自己的缓存,直接读主存

更准确的说法是:

volatile 通过内存屏障和 CPU 缓存一致性机制,保证变量读写的可见性和有序性,但不会真正关闭 CPU 缓存。

CPU 仍然会使用 L1、L2、L3。


9. volatile 的可见性

java 复制代码
private volatile int flag = 0;

线程 A:

java 复制代码
flag = 1;

线程 B:

java 复制代码
int x = flag;

JMM 保证:

text 复制代码
线程 B 能看到线程 A 对 flag 的写入。

更专业地说:

对 volatile 变量的写,happens-before 后续任意线程对同一个 volatile 变量的读。


10. volatile 的有序性

示例:

java 复制代码
class Demo {
    private int data = 0;
    private volatile boolean ready = false;

    public void writer() {
        data = 100;
        ready = true;
    }

    public void reader() {
        if (ready) {
            System.out.println(data);
        }
    }
}

如果 reader 线程看到:

java 复制代码
ready == true

那么它也能看到:

java 复制代码
data == 100

原因是:

text 复制代码
data = 100 不能被重排序到 ready = true 之后
ready = true 是 volatile 写
reader 读到 ready = true 后,可以看到 volatile 写之前的普通写

11. volatile 不能保证原子性

volatile 不能保证复合操作原子性。

java 复制代码
volatile int count = 0;

count++;

count++ 实际是三步:

text 复制代码
1. 读 count
2. 加 1
3. 写回 count

多个线程同时执行,还是可能丢数据。

正确方式:

java 复制代码
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();

或者:

java 复制代码
synchronized

12. happens-before 是 JMM 的核心

JMM 最重要的概念是:

text 复制代码
happens-before

如果操作 A happens-before 操作 B,那么:

text 复制代码
1. A 的结果对 B 可见
2. A 的执行顺序排在 B 之前

13. 常见 happens-before 规则

13.1 程序顺序规则

同一个线程内,前面的操作 happens-before 后面的操作。

java 复制代码
int a = 1;
int b = 2;

在当前线程里:

text 复制代码
a = 1 happens-before b = 2

13.2 volatile 规则

对 volatile 变量的写,happens-before 后续对这个变量的读。

13.3 锁规则

一个线程释放锁,happens-before 另一个线程获取同一把锁。

java 复制代码
synchronized (lock) {
    value = 10;
}

13.4 线程启动规则

调用 Thread.start() 前的操作,对新线程可见。

13.5 线程 join 规则

线程中的操作 happens-before 其他线程从 join() 返回。


14. synchronized 和 volatile 的区别

能力 volatile synchronized
可见性
有序性
原子性 不保证复合操作 保证同步块互斥
是否阻塞 不阻塞 可能阻塞
适合场景 状态标志、单次发布 多变量一致性、临界区

15. AtomicInteger 和 volatile 的区别

java 复制代码
volatile int count;

只能保证读写可见,不能保证:

java 复制代码
count++

线程安全。

java 复制代码
AtomicInteger count = new AtomicInteger();

可以保证:

java 复制代码
count.incrementAndGet();

是原子操作。


16. 双重检查锁为什么要 volatile

经典单例:

java 复制代码
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;
    }
}

为什么 instance 要加 volatile

因为:

java 复制代码
instance = new Singleton();

可能被拆成:

text 复制代码
1. 分配内存
2. 初始化对象
3. 把对象地址赋给 instance

如果发生重排序,可能变成:

text 复制代码
1. 分配内存
2. 把对象地址赋给 instance
3. 初始化对象

另一个线程看到 instance != null,但对象还没初始化完。

volatile 可以禁止这种危险重排序。


17. 常见误区

误区 1:volatile 会禁用 CPU 缓存

错误。

volatile 不会禁用 CPU 缓存。

它是通过内存屏障和缓存一致性协议保证可见性。

误区 2:工作内存就是 L2 或 L3

错误。

JMM 的工作内存是抽象概念,可能对应寄存器、L1、L2、L3、写缓冲、JIT 临时副本等。

误区 3:volatile 可以替代 synchronized

不完全可以。

volatile 不能保证复合操作原子性,也不能保护多个变量的一致性。

误区 4:volatile 修饰集合就线程安全

错误。

java 复制代码
volatile List<String> list = new ArrayList<>();

这里只保证 list 这个引用的可见性,不保证 ArrayList 内部操作线程安全。


18. 速查表

场景 推荐方式
一个线程通知另一个线程停止 volatile boolean
简单状态发布 volatile
自增计数 AtomicInteger / LongAdder
多变量一致更新 synchronized / Lock
保护临界区 synchronized / Lock
高并发计数 LongAdder
单例双重检查 volatile + synchronized

19. 总结

JMM 是 Java 多线程的内存可见性规则。

它抽象出:

text 复制代码
主内存
工作内存

但这两个概念不等于具体硬件:

text 复制代码
主内存 ≠ 单纯物理 RAM
工作内存 ≠ 固定的 L1/L2/L3

volatile 的作用是:

text 复制代码
1. 保证可见性
2. 禁止特定重排序

但它不能保证:

text 复制代码
复合操作原子性
集合线程安全
多个变量的一致性

一句话:

volatile 不是让线程不用缓存,而是让 JVM 和 CPU 按照 JMM 的规则保证变量读写的可见性和有序性。

相关推荐
码兄科技2 小时前
Java AI智能体开发实战:从零构建企业级智能应用指南
java·开发语言·人工智能
2401_859506242 小时前
AIGC赋能大漆摆件设计:从痛点分析到技术架构与实战验证
java·大数据·人工智能
剑挑星河月2 小时前
54.螺旋矩阵
java·算法·leetcode·矩阵
zh路西法2 小时前
【现代控制理论与卡尔曼滤波】从状态空间到Python仿真实现
开发语言·python
Evand J2 小时前
【论文复现】MATLAB例程,存在测距误差的WSN无锚点分布式自定位,《WSN中存在测距误差的无锚点分布式自定位方法》
开发语言·分布式·matlab·定位·导航·wsn
techdashen2 小时前
kTLS 进入 rustls 组织:把 TLS 的数据面交给内核
开发语言·php
Lhappy嘻嘻3 小时前
Java 并发编程(六)|并发进阶高频:CAS、锁升级
java·开发语言
techdashen3 小时前
Arborium:把 tree-sitter 语法高亮打包成 Rust 文档生态的基础设施
开发语言·后端·rust
要开心吖ZSH3 小时前
MVCC 进阶:快照读 vs 当前读、幻读与 Next-Key Lock
java·数据库·sql·mysql·mvcc