Java内存模型(Java Memory Model,JMM)
JMM 是Java虚拟机(JVM)规范中定义的一组规则和规范,用于描述多线程环境下,Java程序中变量的访问和修改行为,尤其是在并发编程中如何保证内存可见性、原子性和有序性。JMM 是 Java 并发编程的基石,它定义了线程之间如何通过内存进行交互,确保程序在不同平台和处理器架构上具有一致的并发行为。(JMM保证了java并发编程的跨平台)
一、JMM 的核心目标
JMM 的主要目标是解决多线程环境下的三个核心问题:
-
原子性(Atomicity):保证某些操作是不可分割的,要么全部执行,要么全部不执行,不会被其他线程干扰。
-
可见性(Visibility):当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。
-
有序性(Ordering):程序执行的顺序按照代码的先后顺序执行,避免指令重排序导致的逻辑错误。
二、JMM 的基本概念
1. 主内存与工作内存(重点)
JMM 抽象了线程与主内存之间的关系:
-
主内存(Main Memory):所有线程共享的内存区域,存储所有的实例变量、静态变量等。可以类比为计算机中的物理内存。
-
工作内存(Working Memory):每个线程都有自己的工作内存(类似于CPU的寄存器或缓存),线程从主内存中读取变量到自己的工作内存中进行操作,操作完成后再将结果写回主内存。工作内存是线程私有的,线程之间不能直接访问彼此的工作内存。
2. 内存间的交互操作(不是重点)
JMM 定义了八种原子操作,用于线程与主内存之间的交互:
- lock(锁定):作用于主内存的变量,将一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,释放一个变量的锁定状态。
- read(读取):作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存,以便随后的load操作。
- load(载入):作用于工作内存的变量,将read操作得到的值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,将工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的字节码指令时就会执行这个操作。
- assign(赋值):作用于工作内存的变量,将执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,将工作内存中的一个变量的值传送到主内存中,以便随后的write操作。
- write(写入):作用于主内存的变量,将store操作得到的值放入主内存的变量中。
这些操作必须按特定的规则组合执行,以确保内存操作的原子性、可见性和有序性。
三、JMM 与 Java 并发编程的关系
JMM 为 Java 并发编程提供了一套规范,确保在多线程环境下程序的行为是可预测和一致的。它通过定义内存屏障(Memory Barriers)、happens-before 关系等机制,来控制线程间的内存可见性和操作顺序。
1. Happens-Before 原则
Happens-Before 是 JMM 中的一个核心概念,用于判断两个操作之间的执行顺序。如果操作A happens-before 操作B,那么操作A的结果对操作B可见,并且操作A在操作B之前执行。
JMM 定义了以下几种 Happens-Before 关系:
-
程序次序规则(Program Order Rule)
在同一个线程中,按照程序代码的顺序,前面的操作 happens-before 后面的操作。 -
锁定规则(Monitor Lock Rule)
一个线程解锁操作 happens-before 另一个线程对同一个锁的加锁操作。 -
volatile 变量规则(Volatile Variable Rule)
对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。 -
线程启动规则(Thread Start Rule)
Thread 对象的start()
方法调用 happens-before 启动线程中的任何操作。 -
线程终止规则(Thread Termination Rule)
线程中的任何操作 happens-before 其他线程检测到该线程已经终止(通过Thread.join()
方法或Thread.isAlive()
返回 false)。 -
线程中断规则(Thread Interruption Rule)
对线程interrupt()
方法的调用 happens-before 被中断线程检测到中断事件(通过Thread.interrupted()
或Thread.isInterrupted()
)。 -
对象终结规则(Finalizer Rule)
一个对象的初始化完成 happens-before 它的finalize()
方法的开始。 -
传递性(Transitivity)
如果操作A happens-before 操作B,且操作B happens-before 操作C,那么操作A happens-before 操作C。
Happens-Before原则帮助开发者理解和推断多线程程序中的内存可见性和操作顺序,从而编写出正确且高效的并发代码。
2. 内存屏障(Memory Barriers)
虽然 JMM 在规范层面定义了 Happens-Before 关系,但在实际实现中,JVM 会通过插入内存屏障来保证这些关系。内存屏障是一种硬件或软件机制,用于控制指令的执行顺序和内存访问的顺序,防止指令重排序和保证内存可见性。
常见的 memory barriers 包括:
屏障类型 | 作用 | 说明 |
---|---|---|
LoadLoad 屏障 | 确保 Load1 的数据加载先于 Load2 及后续的加载操作 | 防止 Load2 读取到比 Load1 更旧的数据 |
StoreStore 屏障 | 确保 Store1 的数据存储先于 Store2 及后续的存储操作 | 防止 Store2 覆盖 Store1 的数据 |
LoadStore 屏障 | 确保 Load1 的数据加载先于 Store2 及后续的存储操作 | 防止 Store2 存储的数据比 Load1 读取的数据更旧 |
StoreLoad 屏障 | 确保 Store1 的数据存储先于 Load2 及后续的加载操作 | 防止 Load2 读取到比 Store1 更旧的数据(最耗性能) |
这些屏障在不同的处理器架构上有不同的实现方式,JVM 会根据目标平台插入相应的屏障指令,以保证 JMM 的语义。
四、JMM 与 Java 关键字的关系
Java 提供了一些关键字和工具来帮助开发者控制内存可见性和线程同步,这些机制在底层依赖于 JMM 的规范。