【JAVA基础面经】JMM(Java内存模型)

文章目录


前言

JMM是一种抽象理论,管理并发环境下主内存与工作内存的交互,以及线程间通过共享变量通信

JMM(Java Memory Model,Java内存模型) 是 Java 并发编程的核心理论基础。它定义了 Java 虚拟机(JVM)在并发环境下如何与主内存、工作内存进行交互,以及线程间如何通过共享变量实现正确通信。

  • JMM 不是真实存在的硬件内存布局,而是一种抽象规范,它屏蔽了不同操作系统和硬件平台的内存访问差异,保证了 Java 程序在各种平台下并发行为的一致性。
  • JMM 主要解决三大问题:原子性、可见性、有序性

一、JMM 的内存结构:主内存与工作内存

  • 主内存:所有线程共享,存放真正的变量值;
  • 工作内存:每个线程私有,存放从主内存拷贝来的变量副本。线程对变量的所有操作(读取、赋值)必须在工作内存中完成,不能直接读写主内存。

1.主内存与工作内存的 JVM 对应

  • 主内存主要对应 JVM 中堆(Heap)上的对象实例数据、方法区(Method Area)中的静态变量和类信息。这些是线程共享的区域。
  • 工作内存:并不对应 JVM 的某个具体内存区域,而是对 CPU 寄存器、CPU 缓存(L1/L2/L3)、以及线程私有栈上的变量副本 的一个抽象统称。

2.主内存与工作内存交互流程

  • read + load:从主内存读取变量到工作内存。
  • use + assign:在工作内存中使用/赋值。
  • store + write:将工作内存的值写回主内存。

图片来源

3.并发三大特性

a. 原子性(Atomicity)

一个或多个操作要么全部执行且不被中断,要么全不执行。

JMM 保证:基本类型的读写(除了 long/double 非原子性情况)是原子的。但 i++ 这种读-改-写操作不是原子的。

JVM 规范允许将 64 位数据的读写实现为两个独立的 32 位操作。如果线程 A 写 long 时写完了高 32 位但未写低 32 位,此时线程 B 读取,就会得到高 32 位是新值、低 32 位是旧值的脏数据。这种现象称为非原子性。
在 64 位 JVM 上,许多实现将 long/double 读写作为原子操作(因为 64 位 CPU 能一次处理),但 Java 语言规范并未要求,所以跨平台代码不能依赖这一点。

b. 可见性(Visibility)

当一个线程修改了共享变量,其他线程能立即看到修改后的值。

JMM 保证:volatile、synchronized、final(构造完成后)能保证可见性

内存的可见性问题

worker 线程一直使用自己工作内存(CPU 缓存)中的副本,导致读取一直是running = true,进入死循环。

java 复制代码
public class VisibilityProblem {
    private static boolean running = true;  // 不加 volatile

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) {
                // 死循环,可能永远看不到 running 变为 false
            }
            System.out.println("线程退出");
        });
        worker.start();
        Thread.sleep(1000);
        running = false;  // 主线程修改,但 worker 线程可能一直使用缓存值
        System.out.println("已设置 running = false");
    }
}

while (running) { } 在 HotSpot JVM 中被 JIT 编译后,可能被优化为:

java 复制代码
if (running) {
    while (true) { }   // 无限循环,不再检查 running
}

添加 volatile 后,volatile 告诉 JVM 和 CPU,这个变量是共享且易变的,禁止使用缓存优化,

  • 每次读 volatile 变量,JVM 都会插入一条 LoadLoad 屏障,强制从主内存加载最新值。
  • 每次写 volatile 变量,JVM 都会插入一条 StoreStore 屏障,强制将新值立即写回主内存,并使其他 CPU 缓存中的对应副本失效。
  • 因此 worker 线程每次循环都会去主内存读取 running,自然能看到主线程的修改
java 复制代码
private static volatile boolean running = true;  // 添加 volatile 后可解决

c. 有序性(Ordering)

程序执行的顺序按照代码的先后顺序执行(禁止指令重排序)。

JMM 保证:volatile 禁止指令重排序,synchronized 保证临界区内代码相对有序(但内部仍可能重排,只是结果不影响单线程视角)。

指令重排序问题

指令重排序:编译器和处理器为了优化性能,可能会改变指令执行顺序,但会遵守 as-if-serial 语义(单线程下结果不变)。多线程环境下重排序可能导致诡异问题。

对于下面的代码,new Singleton() 可以分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

编译器和 CPU 可能为了优化,将 步骤 2 和步骤 3 交换(单线程下无影响),此时存在线程A、B均调用 getInstance() 方法

  • A线程:刚执行完步骤 3(还没初始化)
  • B线程: 过来看到 instance != null 直接返回了
java 复制代码
public class Singleton {
    private static Singleton instance;  // 不加 volatile

    public static Singleton getInstance() {
        if (instance == null) {               // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {       // 第二次检查
                    instance = new Singleton(); // 可能发生指令重排
                }
            }
        }
        return instance;
    }
}
java 复制代码
private static volatile Singleton instance;  // 加上 volatile 禁止重排序

该代码属于单例模式中的懒汉模式,详情可参考:线程安全的单例模式

二、JMM管理线程之间的通信

Java 并发采用 共享内存 模型进行线程间通信。线程之间通过读写公共的共享变量(位于主内存)来隐式传递消息。

  • 发送消息:线程 A 将修改后的共享变量值从工作内存刷新到主内存。

  • 接收消息:线程 B 从主内存重新加载共享变量到自己的工作内存。

这种通信是隐式的,开发者需要通过 synchronized、volatile 或 final 来确保通信的正确性。与显式的消息传递(如 wait/notify、BlockingQueue)不同,共享内存通信更容易出错,但性能更高。

Java线程之间通信的其他方式(待更新)

三、JMM 的核心概念------happens-before 规则

happens-before 是 JMM 中判断两个操作是否具备可见性和有序性的准则。如果操作 A happens-before 操作 B,那么:

  • A 的执行结果对 B 可见(B 能读到 A 写入的值)。
  • 在逻辑顺序上,A 排在 B 之前(尽管实际执行可能被重排,但结果必须符合 happens-before 的顺序)。

注意:happens-before 不是时间上的先后,而是逻辑上的先后约束。

核心规则列表

  1. 程序次序规则:同一个线程内,书写在前的代码 happens-before 书写在后的代码。

  2. 管程锁定规则:对同一个锁的 unlock() happens-before 后续对该锁的 lock()。

  3. volatile 变量规则:对一个 volatile 变量的写 happens-before 后续对该变量的读。

  4. 线程启动规则:Thread.start() happens-before 新线程内的任何动作。

  5. 线程终止规则:线程内的任何动作 happens-before 其他线程检测到该线程终止(如 join() 返回或 isAlive() 返回 false)。

  6. 中断规则:调用 interrupt() happens-before 被中断线程检测到中断事件(抛出 InterruptedException 或 isInterrupted() 返回 true)。

  7. 对象终结规则:对象的构造方法结束 happens-before finalize() 方法的开始。

  8. 传递性:如果 A happens-before B,B happens-before C,则 A happens-before C。

例1:volatile 变量的先写后读

java 复制代码
int a = 0;
volatile boolean flag = false;

// Thread A 线程 A
a = 1;          // 普通写
flag = true;    // volatile 写

// Thread B 线程 B
if (flag) {     // volatile 读
    System.out.println(a);  // 保证看到 a=1
}

因为 flag = true happens-before 线程 B 读取 flag 为真,且 a = 1 happens-before flag = true(程序次序),由传递性可得 a = 1 happens-before 读取 a,因此 B 总能看见 a=1。

面试问题

  1. JMM 和 JVM 内存模型(堆、栈、方法区)有什么区别?
  • JMM 关注的是并发中的可见性、有序性、原子性,是抽象概念。
  • JVM 内存模型(运行时数据区)描述的是类实例、方法、引用等物理存储区域(堆、栈、程序计数器等)。两者不同维度,不要混淆。
  1. i++ 在 JMM 中为什么不安全?

i++ 包含三步:读 i 的值 → 加 1 → 写回。多线程下可能交错执行:线程 A 读 i=0,线程 B 也读 i=0,各自加 1 后写回,结果 i=1 而非 2。这就是原子性问题。

  1. volatile 与 synchronized 的区别?
维度 volatile synchronized
原子性 不保证(复合操作如 i++ 不安全) 保证(被锁保护的代码块原子执行)
可见性 保证(写立即刷新主存,读从主存取) 保证(锁释放前刷新主存,锁获取时重读)
有序性 禁止指令重排序(通过内存屏障) 临界区内代码相对有序(但内部仍可能重排,不影响单线程结果)
作用对象 只能修饰变量 修饰方法或代码块
锁机制 无锁,基于 CPU 缓存一致性(MESI)和内存屏障 互斥锁(悲观锁),阻塞其他线程
性能 轻量级,无阻塞开销 重量级,可能引起线程阻塞和上下文切换
适用场景 一写多读的状态标志位、双重检查锁单例 需要原子性操作的复合操作、互斥访问共享资源
相关推荐
小小仙。2 小时前
IT自学第三十八天
java·开发语言
一定要AK2 小时前
SSM 整合实战—— IDEA 版
java·ide·intellij-idea
XMYX-02 小时前
05 - Go 的循环与判断:语法、用法与最佳实践
开发语言·golang
fengci.2 小时前
php反序列化(复习)(第三章)
android·开发语言·学习·php
echome8882 小时前
Python 装饰器详解:从入门到精通的 7 个实用案例
开发语言·python
子木HAPPY阳VIP2 小时前
【无标题】
java·python·mysql
砍材农夫2 小时前
spring-ai 第七模型介绍-向量模型
java·人工智能·spring
竹之却2 小时前
【Agent-阿程】openclaw v2026.4.9更新内容介绍
开发语言·php·openclaw·openclaw 更新
玛卡巴卡ldf2 小时前
【Springboot6】内存泄漏OOM、VisualVM、Arthas、Prometheus Grafana监控、垃圾回收
java·jvm·springboot