返回文章列表

文章

关于Java内存模型

目录
  1. 以下内容对 Java 内存模型(Java Memory Model,JMM)做全面详解,涵盖其设计目标、核心概念、关键规则,以及常见并发场景下的应用与示例。
  2. 一、设计目标
  3. 二、核心概念
  4. 每个线程只能对自己的工作内存进行读写;对主内存的访问,必须通过内存交互操作完成。
  5. 三、happens‑before 原则
  6. 四、原子性与复合操作
  7. count++; // 包含读、+1、写 三步,非原子 ``` 需要通过同步(synchronized/Lock)或原子类(AtomicInteger)来保证。
  8. 五、可见性机制
  9. 六、内存屏障(Fence)
  10. 七、禁止“出乎意料的”行为
  11. JMM 保证不出现“薄空气读”(out‑of‑thin‑air),即禁止编译器或 CPU 在无任何写入的情况下伪造一个值。
  12. 八、常见示例
  13. 1. 双重检查锁(DCL)
  14. 2. 发布逸出(Publication Escape)
  15. 需使用 volatile 或在同步块中发布,避免“半初始化”对象被其他线程使用。
  16. 九、最佳实践
  17. 📎 参考文章

以下内容对 Java 内存模型(Java Memory Model,JMM)做全面详解,涵盖其设计目标、核心概念、关键规则,以及常见并发场景下的应用与示例。#

一、设计目标#

  1. 可见性(Visibility) 保证一个线程对共享变量的写入,对其他线程最终是可见的。
  2. 有序性(Ordering) 在单线程中,程序执行具有先后顺序;在多线程中,JMM 定义了“happens‑before”规则来约束跨线程的执行顺序。
  3. 原子性(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 可见且顺序有保证。主要规则有:

  1. 程序顺序规则 在同一线程中,语句按程序顺序先行发生。
  2. 监视器锁规则 对某个对象的 unlock 先行发生于随后对同一对象的 lock
  3. volatile 规则 对一个 volatile 变量的写,先行发生于对该变量后续的读。
  4. 线程启动规则 Thread.start() 先行发生于该线程内部的任意操作。
  5. 线程终结规则 该线程的任意操作先行发生于另一线程对其 Thread.join() 返回。
  6. 传递性 如果 A happens‑before B,且 B happens‑before C,则 A happens‑before C。

四、原子性与复合操作#

  • 原子操作
    • 基本类型的单次读/写(int、引用等)是原子的。
    • longdouble 的读/写在 JDK ≥1.5 环境中也已保证原子。
  • 复合操作

count++; // 包含读、+1、写 三步,非原子 ``` 需要通过同步(synchronized/Lock)或原子类(AtomicInteger)来保证。#

五、可见性机制#

  1. volatile** 关键字**
    • 保证写入后立即刷新主内存,读取时总是从主内存拉最新值。
    • 同时具有内存屏障效果:
      • volatile write 向后不重排
      • volatile read 向前不重排
  2. synchronized** / **Lock
    • 进入同步块前,线程会从主内存刷新共享变量到工作内存;
    • 退出同步块后,线程会将修改过的共享变量刷新回主内存。

六、内存屏障(Fence)#

在底层,JMM 通过 CPU 的内存屏障指令来实现重排序限制和可见性保障,典型屏障有:

  • LoadLoadLoadStore
  • StoreLoad(最昂贵,用于 volatile writevolatile 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 或在同步块中发布,避免“半初始化”对象被其他线程使用。#

九、最佳实践#

  1. 优先使用高层并发工具 java.util.concurrent 中的原子类、并发容器、Executor 框架,减少手写锁的错误风险。
  2. **尽量少用 **volatile 仅在确切知道它的可见性+有序性语义足以满足需求时使用。
  3. **DCL 单例请务必加 **volatile
  4. 复杂场景下,写出清晰的 happens‑before 注释 对关键逻辑标注同步意图,方便后续维护。
  5. 充分测试并发边界 使用压力测试、结合工具(如 jcstress)验证内存模型相关假设。

通过以上内容,你应能深入理解 JMM 的设计原则、核心机制以及在实际 Java 并发编程中的应用要点。掌握好可见性、有序性和原子性之间的权衡,才能写出高性能且正确的并发代码。

📎 参考文章#