Java内存模型(JMM)

什么是JMM?

JMM即Java Memory Model

Java 内存模型(JMM) 是一套规范,它规定了多线程环境下:

  1. 如何安全地操作共享数据(比如变量、对象);
  2. 线程何时能看到其他线程对数据的修改(可见性);
  3. 代码的执行顺序如何被正确维护(有序性)。

一句话总结:JMM 是多线程编程的“交通规则”。

它告诉程序在并发场景中:

  • 什么操作必须按顺序执行(禁止重排序)
  • 什么时刻必须刷新数据(保证可见性)
  • 什么情况下操作必须完整执行(原子性)

理解JMM,首先需要明白java的运行时数据区,如下图:

  • 方法区:存储了运行时常量池、字段和方法参数、构造方法等字节码数据。
  • 堆:几乎所有的对象实例及数组都存在这里。
  • 栈:每一个线程有一个私有的栈,用于存储局部变量、栈帧、动态链接、方法出口等。
  • 本地方法栈:与栈类似,不过进入本地方法栈的数据需要通过native关键字表明。
  • 程序计数器:每个线程都有一个独立的程序计数器,用于表示当前线程执行到了字节码的哪一行。

JMM 的工作原理(图解)

对线程来说,栈中的数据不会在线程之间共享,堆中的数据被所有线程共享。所以线程中的可见性问题就是在堆中产生的。

下图中的主存就是堆(这里只是换了一种叫法)存放了线程们共享的变量,线程自己的工作内存中存放了只有自己能访问的变量。

JMM有一个规定:线程对共享变量的所有操作必须在自己的工作内存中执行,不能直接到主存操作或者自己从主存中读取。

为什么需要 JMM?

例子1:

假设你和同事使用Git提交代码:

  • 主内存:远程仓库(公司仓库)的代码(唯一权威版本)
  • 工作内存:每个人本地的Git仓库
  • 问题:你修改了本地代码并提交到了仓库,但未同步到远程仓库,同事到远程仓库看到的是旧版本,而不是最新的代码。

这就是 JMM 要解决的核心问题:多线程环境下,如何保证共享变量的可见性和执行顺序。

例子2:

假如线程1想要访问线程2的变量值有以下步骤(这个过程就是线程之间的通信):

前提:线程2将自己工作内存的值放到了主存中。

  1. 线程1到工作内存中查找
  2. 工作内存到主存中查找
  3. 如果主存中没有则找不到。

这里有一个问题,线程2什么时候将自己工作内存的变量放到放到主存呢?

这是通过JMM来控制的。


JMM三大特征

特性说明保障手段
原子性操作不可分割(要么全做,要么不做)synchronized、Lock
可见性线程修改后其他线程立即可见volatile、synchronized
有序性程序执行顺序符合预期volatile、happens-before

JMM 与运行时内存区的区别

JMM(Java Memory Model)Java 运行时内存区域
定位并发编程的规范(是一个抽象概念)内存物理划分(是真实的内存区域)
核心多线程之间变量的访问规则内存空间
主要内容原子性、可见性、有序性堆、栈、方法区等内存划分
典型问题指令重排导致结果异常内存溢出、垃圾回收

指令重排序的真相

为什么会重排序?

在保证结果正确的前提下提升效率

假设你要按顺序完成三件事:

  1. 烧水(5分钟)
  2. 洗碗(3分钟)
  3. 泡茶(需要烧好水)

优化后的顺序:先烧水 → 趁烧水时洗碗 → 水开后泡茶

这就是典型的指令重排序——在保证结果正确的前提下提升效率。


happens-before 规则(核心!)

为什么有happens-before

  1. 开发者需要 JMM 提供一个强大的内存模型来编写代码
  2. 编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能。

所以,JMM 提供了happens-before 规则(JSR-133 规范),我们只要遵循 happens-before 规则,那么写的程序就能保证在 JMM 中具有强的内存可见性。

1. 八大规则详解
规则说明代码示例
程序顺序规则代码顺序保证结果单线程代码顺序执行
锁规则解锁先于后续加锁synchronized块
volatile规则写操作先于后续读操作volatile变量读写
传递性规则A→B且B→C,则A→C组合多个规则
线程启动规则start()先于线程内任何操作thread.start()
线程终止规则线程最后操作先于终止检测thread.join()
中断规则interrupt()先于检测中断thread.interrupt()
对象终结规则构造函数先于finalize()对象初始化
2. 案例解析
// 正确使用 volatile 的案例
public class VolatileDemo {
    private volatile boolean flag = false;
    private int value = 0;

    public void writer() {
        value = 42;      // 普通写
        flag = true;     // volatile写(happens-before后续读)
    }

    public void reader() {
        if (flag) {      // volatile读
            System.out.println(value); // 一定能看到42
        }
    }
}

通过 volatile 写操作建立 happens-before 关系,保证普通变量的可见性。


JMM 实现原理

1. 内存屏障类型
屏障类型作用
LoadLoad屏障禁止读与读重排序
StoreStore屏障禁止写与写重排序
LoadStore屏障禁止读与写重排序
StoreLoad屏障禁止写与读重排序(全能屏障)
2. volatile 实现原理
// 查看汇编指令(使用hsdis插件)
public class VolatileTest {
    private volatile int v;
  
    public void write() {
        v = 1; // 会插入StoreStore + StoreLoad屏障
    }
  
    public void read() {
        int tmp = v; // 会插入LoadLoad + LoadStore屏障
    }
}

面试常见问题

1:synchronized 能保证可见性吗?

答:可以!因为:

  1. 解锁前必须把变量刷回主内存
  2. 加锁时会清空工作内存,从主内存重新加载
2:final 关键字的内存语义?
  • 构造函数内对 final 域的写入,与后续引用赋值不能重排序
  • 初次读取包含 final 域的对象引用时,会保证 final 域已初始化

不加 volatile 时,可能返回未初始化完成的对象(对象创建的非原子操作)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值