Java内存模型(JMM)详解

一、JMM核心概念
1. 主内存与工作内存

JMM抽象模型

  • 主内存:所有线程共享的内存区域,存储对象实例、静态变量、数组等
  • 工作内存:每个线程独占的内存区域,存储主内存中变量的副本
  • 交互规则:线程对变量的操作(读取、赋值)必须在工作内存中进行,不能直接操作主内存

内存交互操作

  • read:将主内存变量读取到工作内存
  • load:将read的变量值存入工作内存的变量副本
  • use:将工作内存变量值传递给执行引擎
  • assign:将执行引擎结果赋值给工作内存变量
  • store:将工作内存变量值写入主内存
  • write:将store的变量值存入主内存的变量中
2. 三大特性定义
特性 定义 表现
原子性 一个操作或多个操作要么全部执行完成,要么全部不执行 i++ 不是原子操作(包含read/load/use/assign/store/write)
可见性 一个线程对变量的修改,其他线程能立即看到 线程A修改x=1,线程B可能看不到
有序性 程序执行的顺序按照代码的先后顺序执行 编译器/处理器可能重排序指令,导致执行顺序与代码顺序不一致
3. 三大特性实现机制(面试重点:JMM如何保证原子性、可见性、有序性?)

原子性实现

  1. synchronized关键字
    • 原理:通过监视器锁实现,进入同步块获取锁,退出同步块释放锁
    • 范围:同步块内的所有操作具有原子性
  2. Lock接口
    • ReentrantLock,原理类似synchronized,但提供更灵活的锁操作
  3. 原子类
    • AtomicInteger,基于CAS操作实现,保证单个变量的原子更新
    • 适用场景:简单变量的原子操作,性能优于锁

可见性实现

  1. volatile关键字
    • 原理:
      • 写操作:修改工作内存变量后,立即刷新到主内存
      • 读操作:读取工作内存变量前,先从主内存刷新最新值
    • 作用:保证变量的可见性,禁止指令重排序
  2. synchronized关键字
    • 原理:释放锁时,将工作内存变量刷新到主内存
    • 作用:同步块结束时,保证变量的可见性
  3. final关键字
    • 原理:final变量初始化完成后,值不能修改,且对其他线程可见
    • 作用:保证final变量的可见性

有序性实现

  1. volatile关键字
    • 原理:通过内存屏障禁止指令重排序
    • 作用:保证volatile变量前后的指令不会被重排序
  2. synchronized关键字
    • 原理:同步块内的操作作为一个整体执行,不会被重排序
    • 作用:保证同步块内的操作有序性
  3. happens-before原则
    • 原理:通过规则定义操作间的顺序关系
    • 作用:无需额外同步,保证操作的有序性
二、happens-before原则
1. 定义

happens-before原则是JMM定义的线程间操作的顺序关系 ,用于保证多线程环境下的可见性有序性。如果操作A happens-before操作B,那么A的执行结果对B可见,且A的执行顺序在B之前。

2. 8条规则(面试重点:请列举happens-before原则的几条重要规则)

1. 程序顺序规则

  • 同一线程内,按照代码顺序,前面的操作happens-before后面的操作

  • 示例:

    java 复制代码
    int a = 1; // 操作A
    int b = a + 1; // 操作B
    // A happens-before B,B能看到A的结果

2. 监视器锁规则

  • 对同一个锁的解锁操作 happens-before后续对该锁的加锁操作

  • 示例:

    java 复制代码
    synchronized (lock) { // 加锁
        x = 1; // 操作A
    } // 解锁(操作B)
    
    synchronized (lock) { // 加锁(操作C)
        y = x; // 操作D,能看到A的结果
    }
    // B happens-before C,D能看到A的结果

3. volatile变量规则

  • 对volatile变量的写操作 happens-before后续对该变量的读操作

  • 示例:

    java 复制代码
    volatile int x = 0;
    
    Thread A:
    x = 1; // 操作A(写volatile)
    
    Thread B:
    int y = x; // 操作B(读volatile),y=1
    // A happens-before B,B能看到A的结果

4. 线程启动规则

  • Thread.start()操作happens-before线程内的任何操作

  • 示例:

    java 复制代码
    Thread t = new Thread(() -> {
        System.out.println(x); // 能看到x=1
    });
    x = 1; // 操作A
    t.start(); // 操作B
    // A happens-before B,线程内操作能看到A的结果

5. 线程终止规则

  • 线程内的所有操作happens-before其他线程检测到该线程终止
  • 示例:通过Thread.join()Thread.isAlive()检测线程终止

6. 线程中断规则

  • 对线程的interrupt()操作happens-before被中断线程检测到中断事件
  • 示例:通过Thread.interrupted()检测中断

7. 对象终结规则

  • 对象的构造函数执行完成happens-before对象的finalize()方法执行
  • 示例:对象初始化完成后,finalize()方法才能执行

8. 传递性规则

  • 若A happens-before B,B happens-before C,则A happens-before C
  • 示例:结合volatile变量规则和程序顺序规则,保证跨线程操作的可见性
3. 作用
  • 简化同步编程:无需显式同步,通过规则保证可见性和有序性
  • 指导编译器优化:编译器在不违反happens-before原则的前提下,可以进行重排序
  • 保证多线程正确性:确保线程间操作的顺序关系,避免数据竞争
三、指令重排序
1. 定义

指令重排序是指编译器或处理器为了优化性能,改变指令的执行顺序,但不改变程序的语义(单线程下的执行结果不变)。

2. 分类

1. 编译器重排序

  • 发生阶段:编译时,由编译器优化

  • 目的:提高指令执行效率,减少CPU空闲时间

  • 示例

    java 复制代码
    // 代码顺序
    int a = 1; // 操作A
    int b = 2; // 操作B
    // 编译器可能重排序为 B → A,单线程结果不变
  • 解决方法 :编译器内存屏障(如volatile关键字禁止重排序)

2. 处理器重排序

  • 发生阶段:CPU执行时,由处理器优化

  • 类型

    • 指令级并行重排序:CPU流水线并行执行指令
    • 乱序执行引擎重排序:CPU动态调整指令执行顺序
  • 示例

    java 复制代码
    // 代码顺序
    int a = 1; // 操作A(依赖内存)
    int b = a + 1; // 操作B(依赖A)
    int c = 2; // 操作C(无依赖)
    // 处理器可能重排序为 A → C → B,因为C无依赖,可并行执行
  • 解决方法 :处理器内存屏障(如lfencesfencemfence指令)

3. 内存重排序

  • 发生阶段:内存访问时,由于缓存一致性协议导致

  • 表现

    • 写缓冲:CPU写操作先存入写缓冲,稍后刷入主内存
    • 无效队列:CPU收到失效消息,先存入无效队列,稍后处理
  • 示例

    java 复制代码
    // 线程A
    x = 1; // 写x(存入写缓冲)
    int r1 = y; // 读y(可能读到旧值)
    
    // 线程B
    y = 1; // 写y(存入写缓冲)
    int r2 = x; // 读x(可能读到旧值)
    // 可能出现r1=0且r2=0的情况,这是内存重排序导致的
  • 解决方法:内存屏障,强制刷新写缓冲或清空无效队列

3. 内存屏障(解决重排序的关键)

定义 :内存屏障是一条指令,用于禁止特定类型的指令重排序,并保证内存可见性。

分类

屏障类型 作用 示例指令
LoadLoad Barriers 禁止LOAD1 → LOAD2重排序 lfence
StoreStore Barriers 禁止STORE1 → STORE2重排序 sfence
LoadStore Barriers 禁止LOAD1 → STORE2重排序
StoreLoad Barriers 禁止STORE1 → LOAD2重排序(最强屏障) mfence

volatile的内存屏障实现

  • 写操作后:插入StoreStore屏障 + StoreLoad屏障
  • 读操作前:插入LoadLoad屏障 + LoadStore屏障

synchronized的内存屏障实现

  • 进入同步块:插入LoadLoad屏障 + LoadStore屏障
  • 退出同步块:插入StoreStore屏障 + StoreLoad屏障
四、面试题解答
1. JMM如何保证原子性、可见性、有序性?

原子性

  • 通过synchronizedLock接口保证:同步块内的操作作为一个整体执行,不可中断
  • 通过原子类保证:基于CAS操作,保证单个变量的原子更新

可见性

  • 通过volatile保证:写操作立即刷新到主内存,读操作先从主内存刷新
  • 通过synchronized保证:释放锁时刷新到主内存,获取锁时从主内存读取
  • 通过final保证:final变量初始化完成后,对其他线程可见

有序性

  • 通过volatile保证:禁止指令重排序,确保volatile变量前后的指令顺序
  • 通过synchronized保证:同步块内的操作作为一个整体,不会被重排序
  • 通过happens-before原则保证:无需显式同步,通过规则定义操作间的顺序关系
2. 请列举happens-before原则的几条重要规则。

重要规则

  1. 程序顺序规则:同一线程内,代码顺序决定操作顺序
  2. 监视器锁规则:解锁happens-before后续加锁
  3. volatile变量规则:写volatile happens-before后续读volatile
  4. 线程启动规则:start() happens-before线程内操作
  5. 传递性规则:A happens-before B,B happens-before C → A happens-before C
五、总结

Java内存模型(JMM)是Java并发编程的基础,通过主内存/工作内存模型三大特性happens-before原则指令重排序等概念,保证多线程环境下的程序正确性。理解JMM的核心机制,对于编写高效、安全的并发程序至关重要,也是面试中的高频考点。

核心要点

  • JMM通过synchronizedvolatile原子类等机制保证原子性、可见性、有序性
  • happens-before原则定义了线程间操作的顺序关系,简化同步编程
  • 指令重排序 是性能优化的结果,通过内存屏障解决其带来的并发问题

掌握JMM的原理,能够帮助开发者理解并发问题的根源,选择合适的同步机制,编写高效、可靠的并发程序。

相关推荐
谈笑也风生2 小时前
经典算法题型之复数乘法(二)
开发语言·python·算法
hkNaruto2 小时前
【C++】记录一次C++程序编译缓慢原因分析——滥用stdafx.h公共头文件
开发语言·c++
czhc11400756632 小时前
C# 1221
java·servlet·c#
先知后行。2 小时前
python的类
开发语言·python
黄俊懿2 小时前
【深入理解SpringCloud微服务】Seata(AT模式)源码解析——全局事务的回滚
java·后端·spring·spring cloud·微服务·架构·架构师
派大鑫wink2 小时前
【Day12】String 类详解:不可变性、常用方法与字符串拼接优化
java·开发语言
JIngJaneIL2 小时前
基于springboot + vue健康管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端
秋饼2 小时前
【三大锁王争霸赛:Java锁、数据库锁、分布式锁谁是卷王?】
java·数据库·分布式
dyxal2 小时前
Python包导入终极指南:子文件如何成功调用父目录模块
开发语言·python