【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)和内存屏障 互斥锁(悲观锁),阻塞其他线程
性能 轻量级,无阻塞开销 重量级,可能引起线程阻塞和上下文切换
适用场景 一写多读的状态标志位、双重检查锁单例 需要原子性操作的复合操作、互斥访问共享资源
相关推荐
Flittly17 小时前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
小兔崽子去哪了17 小时前
Java 生成二维码解决方案
java·后端
人活一口气1 天前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
NE_STOP1 天前
Vibe Coding -- 完整项目案例实操
java
荣码1 天前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python
SimonKing1 天前
Google第三方授权登录
java·后端·程序员
明月光8181 天前
从一行 @Builder 说起:重新拾起 Java 的 Lombok、注解与 Builder 模式
java
考虑考虑1 天前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯1 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
青石路2 天前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java