JVMJMM 是 Java 中两个核心但又经常混淆的概念。简单来说:

JVM 是“运行时环境”,JMM 是“并发规则”。

下面我们从多个维度来详细对比:


一、核心定义对比

维度

JVM (Java Virtual Machine)

JMM (Java Memory Model)

本质

一个具体的软件/规范,是 Java 程序运行的平台和环境。

一组抽象的规则和规范,定义多线程下内存访问的行为。

类比

就像一个完整的工厂,有厂房、生产线、仓库、物流等实体设施。

就像工厂里的《安全生产手册》和《协同工作协议》,规定了工人之间如何传递物料、如何避免冲突。

目标

“一次编写,到处运行”,提供与平台无关的运行环境。

在多线程环境下,为程序员提供一致、可预测的内存可见性和有序性保证


二、职责与功能对比

JVM 的职责(负责“做”)
  1. 类加载:将 .class文件加载到内存。

  2. 内存管理:分配和管理真实的物理内存区域,如:

    • :存放对象实例。

    • 方法区:存放类信息、常量、静态变量。

    • 虚拟机栈:存放局部变量、方法调用栈帧。

    • 本地方法栈程序计数器

  3. 字节码执行:通过解释器或 JIT 编译器执行字节码。

  4. 垃圾回收:自动回收堆中不再使用的对象。

  5. 本地接口:与 Native 方法交互。

JMM 的职责(负责“管规则”)
  1. 定义线程与主内存的交互:规定变量如何从主内存复制到工作内存,以及如何同步回主内存。

  2. 提供内存可见性保证:确保一个线程的修改能被其他线程及时看到(通过 volatilesynchronized等规则)。

  3. 提供有序性保证:禁止特定类型的指令重排序(通过 Happens-Before 规则​ 和内存屏障)。

  4. 提供同步工具:定义了 synchronizedvolatilefinal等关键字在多线程环境下的语义

关键区别:JVM 管理的堆、栈是真实的内存分区,是物理或逻辑上真实存在的。而 JMM 中的主内存、工作内存是抽象概念,前者主要对应堆,后者对应CPU缓存、寄存器、栈的一部分,但并非一一对应。

三、一个生动的类比

场景:一个大型图书馆(类比JVM)

  • JVM 是这个图书馆本身

    • 它有真实的书库(堆)​ 存放所有书籍(对象)。

    • 它有阅览桌(栈)​ 供读者临时阅读。

    • 它有图书管理员(GC)​ 定期清理没人看的书。

    • 它有借还书系统(执行引擎)​ 处理读者的请求。

  • JMM 是图书馆的《借阅管理规定》

    • 规定多个读者(线程)​ 想同时看同一本书时怎么办。

    • 规定一个读者在书上做了笔记(修改数据),如何让其他读者立刻看到(可见性)。

    • 规定读者必须“先借书,后阅读”,这个顺序不能乱(有序性,防止重排序)。

    • 规定一本书同一时间只能被一个读者借出做笔记(原子性,通过 synchronized实现互斥)。

没有 JMM:所有读者一窝蜂冲进书库抢书、乱写乱画,结果一片混乱,谁也不知道书的最新内容是什么。

有了 JMM:所有读者都按规则办事,图书馆秩序井然,信息传递高效准确。

四、JMM 的核心内容

JMM 主要围绕“线程如何与主内存交互”以及“线程间如何通信”来定义规则。它规定了以下三件事:

1. 原子性(Atomicity)
  • 定义:一个或多个操作,要么全部执行且不会被打断,要么都不执行。

  • JMM 的保证:对基本类型(long, double除外)的读取和赋值是原子性的。例如 int a = 10;是原子的。

  • 常见误区i++操作不是原子的,它涉及读取、计算、写入三个步骤。需要使用 synchronizedAtomicInteger来保证其原子性。

2. 可见性(Visibility)
  • 定义:一个线程修改了共享变量的值,其他线程能立即看到修改后的值。

  • 实现机制

    • volatile关键字:当写一个 volatile变量时,JMM 会立即将该线程本地内存中的值刷新到主内存。当读一个 volatile变量时,JMM 会使该线程的本地内存无效,强制从主内存重新读取

    • synchronized关键字:在解锁前,必须将共享变量同步到主内存。在加锁时,会清空本地内存,从主内存重新加载。

    • final关键字:被 final修饰的字段,在构造器初始化完成后,对其他线程是可见的(前提是对象引用没有“溢出”)。

3. 有序性(Ordering)
  • 定义:程序执行的顺序按照代码的先后顺序执行。禁止指令重排序。

  • 实现机制

    • volatile关键字:通过插入内存屏障,禁止在 volatile写之前的指令被重排序到写之后,也禁止在 volatile读之后的指令被重排序到读之前。

    • synchronized关键字:一个变量在同一个时刻只允许一条线程对其进行 lock操作,这使得持有同一个锁的两个同步块只能串行进入,从而保证了有序性。

    • Happens-Before 原则:这是 JMM 的灵魂和最核心的部分。

五、Happens-Before 原则

这是一组天然存在的、无需任何同步协助就能保证可见性和有序性的规则。如果操作 A “Happens-Before” 于操作 B,那么 A 所做的任何修改对 B 都是可见的,且 A 的执行顺序排在 B 之前。

  1. 程序次序规则:在同一个线程中,前面的操作 Happens-Before 于后面的任何操作。

  2. 监视器锁规则:一个 unlock操作 Happens-Before 于后续对同一个锁lock操作。

  3. volatile变量规则:对一个 volatile变量的操作 Happens-Before 于后续对这个变量的操作。

  4. 传递性:如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

  5. 线程启动规则Thread.start()调用 Happens-Before 于这个线程内的任何操作。

  6. 线程终止规则:线程中的任何操作都 Happens-Before 于其他线程检测到该线程已经终止(如 Thread.join()成功返回,Thread.isAlive()返回 false)。

  7. 对象终结规则:一个对象的构造器结束 Happens-Before 于它的 finalize()方法开始。

  8. 线程中断规则:对线程 interrupt()的调用 Happens-Before 于被中断线程检测到中断事件。

注意:Happens-Before 并不一定意味着时间上的先后,它强调的是前一个操作的结果对后一个操作可见

六、JMM 的关键抽象:主内存与工作内存

JMM 定义了以下抽象结构:

  • 主内存:可以粗略地理解为 Java 堆,存储所有共享变量。

  • 工作内存:每个线程私有的。可以粗略理解为 CPU 寄存器、缓存和栈的一部分。它存储了该线程使用到的变量的主内存副本

线程、主内存、工作内存的交互关系

  1. 线程对共享变量的所有操作(读、写)都必须在自己的工作内存中进行。

  2. 线程不能直接读写主内存中的变量,也不能直接访问其他线程的工作内存。

  3. 线程间变量的传递(通信)必须通过主内存来完成。

这个抽象模型是理解 volatilesynchronized等关键字如何工作的基础。

七、volatile关键字在 JMM 中的精确作用

1.保证可见性:如前所述,写操作立即刷新到主内存,读操作使本地内存失效。
2.禁止指令重排序:通过内存屏障实现。
3.不保证原子性:对 volatile变量的复合操作(如 i++)仍然不是原子的。
典型应用场景:
  •状态标志位:private volatile boolean flag;
  •一次性安全发布(结合 final使用):private volatile Singleton instance;(双重检查锁定模式)

JMM 的本质:是一套规范,它规定了 Java 编译器、JVM 运行时和处理器需要遵守什么样的内存访问规则,以在追求性能最大化的同时,为开发者提供一个清晰、一致的并发编程模型。当你理解了 JMM 和 Happens-Before 原则,你就能真正理解 synchronizedvolatilefinal等关键字的深层含义,从而编写出正确、高效的并发程序。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐