文章
关于Java内存模型
目录
- 以下内容对 Java 内存模型(Java Memory Model,JMM)做全面详解,涵盖其设计目标、核心概念、关键规则,以及常见并发场景下的应用与示例。
- 一、设计目标
- 二、核心概念
- 每个线程只能对自己的工作内存进行读写;对主内存的访问,必须通过内存交互操作完成。
- 三、happens‑before 原则
- 四、原子性与复合操作
- count++; // 包含读、+1、写 三步,非原子 ``` 需要通过同步(synchronized/Lock)或原子类(AtomicInteger)来保证。
- 五、可见性机制
- 六、内存屏障(Fence)
- 七、禁止“出乎意料的”行为
- JMM 保证不出现“薄空气读”(out‑of‑thin‑air),即禁止编译器或 CPU 在无任何写入的情况下伪造一个值。
- 八、常见示例
- 1. 双重检查锁(DCL)
- 2. 发布逸出(Publication Escape)
- 需使用 volatile 或在同步块中发布,避免“半初始化”对象被其他线程使用。
- 九、最佳实践
- 📎 参考文章
以下内容对 Java 内存模型(Java Memory Model,JMM)做全面详解,涵盖其设计目标、核心概念、关键规则,以及常见并发场景下的应用与示例。#
一、设计目标#
- 可见性(Visibility) 保证一个线程对共享变量的写入,对其他线程最终是可见的。
- 有序性(Ordering) 在单线程中,程序执行具有先后顺序;在多线程中,JMM 定义了“happens‑before”规则来约束跨线程的执行顺序。
- 原子性(Atomicity)
对基本类型的写操作(long、double 除外)和引用赋值是原子的,但更复杂的复合操作(如
i++)并不原子。
二、核心概念#
| 概念 | 含义 |
| **工作内存** | 每个线程私有的缓存,存放该线程读取的共享变量副本。 |
| **主内存** | 所有线程共享的存储,存放字段和数组元素的“真实值”。 |
| **内存交互操作** | `lock`、`unlock`、`read`、`write`、`volatile read`、`volatile write`、`thread start/join` 等。 |
每个线程只能对自己的工作内存进行读写;对主内存的访问,必须通过内存交互操作完成。#
三、happens‑before 原则#
JMM 通过一系列规则定义“先行发生关系”(happens‑before),它是一种偏序关系,若 A happens‑before B,则对 A 的效果对 B 可见且顺序有保证。主要规则有:
- 程序顺序规则 在同一线程中,语句按程序顺序先行发生。
- 监视器锁规则
对某个对象的
unlock先行发生于随后对同一对象的lock。 - volatile 规则
对一个
volatile变量的写,先行发生于对该变量后续的读。 - 线程启动规则
Thread.start()先行发生于该线程内部的任意操作。 - 线程终结规则
该线程的任意操作先行发生于另一线程对其
Thread.join()返回。 - 传递性 如果 A happens‑before B,且 B happens‑before C,则 A happens‑before C。
四、原子性与复合操作#
- 原子操作
- 基本类型的单次读/写(
int、引用等)是原子的。 long、double的读/写在 JDK ≥1.5 环境中也已保证原子。
- 基本类型的单次读/写(
- 复合操作
count++; // 包含读、+1、写 三步,非原子
```
需要通过同步(synchronized/Lock)或原子类(AtomicInteger)来保证。#
五、可见性机制#
volatile** 关键字**- 保证写入后立即刷新主内存,读取时总是从主内存拉最新值。
- 同时具有内存屏障效果:
- volatile write 向后不重排
- volatile read 向前不重排
synchronized** / **Lock- 进入同步块前,线程会从主内存刷新共享变量到工作内存;
- 退出同步块后,线程会将修改过的共享变量刷新回主内存。
六、内存屏障(Fence)#
在底层,JMM 通过 CPU 的内存屏障指令来实现重排序限制和可见性保障,典型屏障有:
- LoadLoad、LoadStore
- StoreLoad(最昂贵,用于
volatile write到volatile read) - StoreStore
七、禁止“出乎意料的”行为#
JMM 保证不出现“薄空气读”(out‑of‑thin‑air),即禁止编译器或 CPU 在无任何写入的情况下伪造一个值。#
八、常见示例#
1. 双重检查锁(DCL)#
public class Singleton {
private static volatile Singleton INSTANCE;
public static Singleton getInstance() {
if (INSTANCE == null) { // 1
synchronized (Singleton.class) { // 2
if (INSTANCE == null) { // 3
INSTANCE = new Singleton(); // 4
}
}
}
return INSTANCE;
}
}
volatile防止指令重排序: 在创建new Singleton()时会经历(a)分配内存、(b)初始化、(c)赋值引用;若无volatile,可能重排序成 a→c→b,导致其他线程看到非空引用但未初始化完毕。
2. 发布逸出(Publication Escape)#
public class Escape {
public static Escape INSTANCE;
public Escape() { /* 初始化 */ }
public static void init() {
INSTANCE = new Escape(); // 没有同步或 volatile,可能部分初始化对其他线程可见
}
}
需使用 volatile 或在同步块中发布,避免“半初始化”对象被其他线程使用。#
九、最佳实践#
- 优先使用高层并发工具
java.util.concurrent中的原子类、并发容器、Executor框架,减少手写锁的错误风险。 - **尽量少用 **
volatile仅在确切知道它的可见性+有序性语义足以满足需求时使用。 - **DCL 单例请务必加 **
volatile - 复杂场景下,写出清晰的 happens‑before 注释 对关键逻辑标注同步意图,方便后续维护。
- 充分测试并发边界 使用压力测试、结合工具(如 jcstress)验证内存模型相关假设。
通过以上内容,你应能深入理解 JMM 的设计原则、核心机制以及在实际 Java 并发编程中的应用要点。掌握好可见性、有序性和原子性之间的权衡,才能写出高性能且正确的并发代码。