Chapter 01

Java 并发基础与内存模型

理解 JMM、happens-before 规则与 volatile,掌握并发三大核心问题:原子性、可见性、有序性

为什么并发编程如此困难?

初学者往往认为多线程编程只是「多开几个线程、加个锁」这么简单。但实际工程中,并发 Bug 往往是最难复现、最难定位的一类问题——它们可能在测试环境中从不出现,却在生产环境高并发下频繁崩溃。

根本原因在于:现代计算机硬件的实际执行模型与程序员的心智模型之间存在巨大的鸿沟。 CPU 有多级缓存,编译器会重排序指令,内存系统并非严格按照程序书写顺序操作——这些优化在单线程下安全, 但在多线程下可能产生难以预料的结果。

Java 通过 Java 内存模型(JMM, Java Memory Model)规范化了多线程程序的行为, 为开发者提供了一套明确的规则:什么情况下可以安全读写共享变量,什么情况下必须同步。

Java 内存模型(JMM)

JMM 是什么?

JMM 并不是 JVM 的物理内存布局(那是 JVM 内存结构,包含堆、栈、方法区等),而是一套 抽象的内存可见性规范,定义了:

概念辨析:JMM vs JVM 内存结构

很多初学者把两者混淆。JVM 内存结构是 JVM 的物理划分(堆、栈、方法区、PC 寄存器等), 是运行时数据区的分配方式。JMM 是并发编程中的抽象规范,描述线程间共享变量的可见性规则。 两者描述的是不同层面的问题。

主内存与工作内存模型

JMM 将内存抽象为两个层次:

主内存(Main Memory)
所有线程共享的内存区域,存放所有实例变量、静态变量和数组元素。对应物理上的 RAM(但经过 JVM 抽象)。
工作内存(Working Memory)
每个线程独有的本地内存,保存该线程使用的变量的主内存副本(拷贝)。对应 CPU 寄存器和缓存。线程对变量的所有操作都在工作内存中完成,不能直接读写主内存中的变量。
线程 A 主内存 线程 B ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 工作内存 A │ read/ │ │ read/ │ 工作内存 B │ │ │◄──write─► 共享变量 x │◄──write─► │ │ x_copy = 1 │ │ x = 1 │ │ x_copy = ? │ └──────────────┘ └──────────────┘ └──────────────┘ 线程 A 更新了 x,但何时刷新到主内存? 线程 B 何时从主内存读取最新值?——这就是可见性问题!

JMM 的八种操作

JMM 定义了 8 种原子操作,描述线程如何与内存交互:

lock
把主内存中的变量标识为一条线程独占的状态。
unlock
把 lock 状态的变量释放,释放后才能被其他线程 lock。
read
从主内存读取变量值,传输到线程的工作内存,为后续 load 操作准备。
load
把 read 操作传来的值放入工作内存的变量副本中。
use
把工作内存的变量值传递给执行引擎(如计算表达式时读取变量)。
assign
把执行引擎的值赋给工作内存的变量(如变量赋值语句)。
store
把工作内存的变量值传输到主内存,为后续 write 操作准备。
write
把 store 操作传来的值写入主内存的变量中。

并发三大核心问题

1. 原子性(Atomicity)

原子性指一个或多个操作作为一个整体执行,执行过程不能被中断——要么全部完成,要么完全不执行。

最典型的例子是 i++ 操作。看起来是一条语句,实际上分为三步:

  1. 从内存读取 i 的当前值(read + load
  2. 将值加 1(use + assign
  3. 将新值写回内存(store + write

如果两个线程同时执行 i++,线程 A 读取到 i=5,尚未写回时线程 B 也读取到 i=5,两者各自加 1 后都写入 6——结果是 6 而非预期的 7。 这就是丢失更新(Lost Update)问题。

// 非原子操作示例:i++ 在多线程下不安全
public class Counter {
    private int count = 0;

    public void increment() {
        count++; // 非原子!read → add → write 三步可被中断
    }

    public int get() { return count; }
}

// 10个线程各执行1000次 increment,最终结果通常 < 10000

Java 中,对基本类型(byteshortintcharfloatboolean)的读写是原子的,但 longdouble 在 32 位 JVM 上不保证原子性(分两次 32 位操作)。在 64 位 JVM 上通常也是原子的,但 JMM 规范未强制要求。

2. 可见性(Visibility)

可见性指一个线程对共享变量的修改,另一个线程能够立即看到最新值。

由于 CPU 缓存的存在,线程 A 修改的变量可能只更新了 A 的 CPU 缓存,尚未刷新到主内存; 线程 B 从自己的缓存读取,可能拿到旧值——这就是缓存不一致(Cache Incoherence)问题。

// 可见性问题示例
public class VisibilityDemo {
    private static boolean running = true; // 没有 volatile!

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (running) { // 可能永远循环:线程从缓存读到 true
                // do work
            }
            System.out.println("Thread stopped");
        }).start();

        Thread.sleep(100);
        running = false; // 主线程修改,但工作线程可能看不到!
    }
}

3. 有序性(Ordering)

有序性指程序按照代码书写的顺序执行。但实际上,编译器和 CPU 会进行指令重排序 (Instruction Reordering)来优化执行效率,只要不影响单线程执行结果就允许重排。

问题在于,在多线程下,这种重排可能会改变程序的语义:

// 指令重排序导致的问题(经典双检锁问题)
public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {              // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {          // 第二次检查
                    instance = new Singleton(); // 非原子!可被重排序
                    // 正常顺序:1.分配内存 2.初始化对象 3.赋值给 instance
                    // 重排顺序:1.分配内存 2.赋值给 instance 3.初始化对象
                    // 若另一线程在步骤 2 后读取 instance,拿到未初始化的对象!
                }
            }
        }
        return instance;
    }
}
修复方式

instance 声明为 volatile,禁止重排序: private static volatile Singleton instance;

happens-before 规则

JMM 通过 happens-before(先行发生)关系来描述操作之间的内存可见性保证。 如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,且 A 的执行顺序在 B 之前。

happens-before 不代表物理时间上 A 先执行,而是一种内存可见性的保证协议。 JMM 规定了 8 条天然的 happens-before 规则:

程序次序规则
在一个线程内,按照代码书写顺序,前面的操作 happens-before 后面的操作。(注意:是单线程内的语义顺序,允许指令重排,但结果与顺序执行相同)
监视器锁规则
对一个锁的解锁(unlock)happens-before 随后对这个锁的加锁(lock)。即:unlock 之前的所有写操作,对后续获得该锁的线程可见。
volatile 变量规则
对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。这是 volatile 提供可见性保证的理论基础。
线程启动规则
Thread.start() 方法 happens-before 该线程的每一个动作。即:启动线程之前的所有操作对该线程可见。
线程终止规则
线程中所有操作 happens-before 对该线程的 Thread.join() 返回。即:被等待线程的所有操作对调用 join() 后的线程可见。
线程中断规则
对线程 interrupt() 的调用 happens-before 被中断线程检测到中断事件(通过 isInterrupted() 或抛出 InterruptedException)。
对象终结规则
一个对象的初始化完成(构造函数执行结束)happens-before 它的 finalize() 开始。
传递性规则
如果 A happens-before B,且 B happens-before C,则 A happens-before C。这使得 happens-before 关系可以在多个操作之间传递。

happens-before 实战理解

// happens-before 关系示例
class HappensBeforeDemo {
    int x = 0;
    volatile boolean flag = false;

    // 线程 A
    void writer() {
        x = 42;          // 操作 1
        flag = true;     // 操作 2(volatile 写)
    }

    // 线程 B
    void reader() {
        if (flag) {      // 操作 3(volatile 读)
            // 此处能看到 x == 42 吗?
            System.out.println(x); // 操作 4
        }
    }
}
// 分析:
// 操作1 happens-before 操作2(程序次序规则)
// 操作2 happens-before 操作3(volatile 变量规则)
// 操作3 happens-before 操作4(程序次序规则)
// 因此:操作1 happens-before 操作4(传递性)
// 结论:线程 B 在读到 flag==true 后,能看到 x==42 ✓

volatile 关键字

volatile 的两大保证

volatile 关键字是 Java 并发中一个轻量级的同步机制,提供两个核心保证:

可见性保证
对 volatile 变量的写会立即刷新到主内存;对 volatile 变量的读会直接从主内存读取最新值,不使用缓存。这通过内存屏障(Memory Barrier)实现。
有序性保证(禁止重排序)
volatile 写之前的操作不能被重排序到 volatile 写之后;volatile 读之后的操作不能被重排序到 volatile 读之前。相当于在前后分别插入 StoreStore/LoadStore 和 LoadLoad/StoreLoad 内存屏障。
volatile 不保证原子性

volatile int i; i++; 仍然不是原子操作!volatile 只保证单次读或单次写的可见性, 但 i++ 是「读-改-写」的复合操作,需要使用 synchronizedAtomicInteger

volatile 的内存屏障实现

volatile 的实现依赖 CPU 的内存屏障指令(x86 上为 LOCK 前缀或 MFENCE 指令)。 JVM 在 volatile 写前后和读前后插入四种屏障:

volatile 写操作的屏障插入: ┌─────────────────────────────────────────────┐ │ ... 普通写操作 ... │ │ StoreStore Barrier ← 禁止上方写重排到下方 │ │ volatile 写 (store + LOCK) │ │ StoreLoad Barrier ← 禁止下方读重排到上方 │ │ ... 后续操作 ... │ └─────────────────────────────────────────────┘ volatile 读操作的屏障插入: ┌─────────────────────────────────────────────┐ │ ... 前置操作 ... │ │ LoadLoad Barrier ← 禁止下方读重排到上方 │ │ volatile 读 │ │ LoadStore Barrier ← 禁止下方写重排到上方 │ │ ... 后续操作 ... │ └─────────────────────────────────────────────┘

volatile 的适用场景

适合 volatile 的场景

  • 状态标志(如 runninginitialized
  • 双检锁单例中的 instance 变量
  • 一写多读的共享变量
  • 独立观察(一个线程写,其他线程只需读最新值)

不适合 volatile 的场景

  • 需要原子性的「读-改-写」操作(用 AtomicInteger
  • 多线程同时写的场景(用 synchronized 或锁)
  • 需要保证一组操作整体原子性(用 synchronized
  • 复杂的依赖状态更新

内存屏障与 CPU 缓存一致性协议

MESI 协议简介

现代 CPU 采用 MESI 协议(或其变体)来保证多核缓存的一致性。MESI 是四种缓存行状态的缩写:

M — Modified
该缓存行已被修改,数据与主内存不一致。只有本 CPU 持有最新值,其他 CPU 缓存已失效。
E — Exclusive
该缓存行独占,数据与主内存一致,其他 CPU 没有缓存该行。
S — Shared
该缓存行被多个 CPU 共享,数据与主内存一致。
I — Invalid
该缓存行已失效,需要重新从主内存加载。

当 CPU A 修改一个缓存行时(M 状态),会通过总线发送「失效消息」给其他 CPU,将它们的对应缓存行设为 I 状态。 其他 CPU 下次访问该变量时,发现缓存已失效,会重新从主内存(或 CPU A 的缓存)读取最新值。 这就是硬件层面的可见性保证机制。

然而,为了提升 CPU 流水线效率,引入了写缓冲区(Store Buffer)失效队列 (Invalidation Queue),使得 MESI 协议并非严格即时生效——这正是为什么 JMM 需要内存屏障来显式 刷新/排空这些缓冲区。

同步的成本与权衡

理解了并发的三大问题后,我们需要在「正确性」和「性能」之间权衡。常见的同步手段及其开销:

同步手段 保证原子性 保证可见性 保证有序性 性能开销
synchronized 中等(偏向锁→轻量级锁→重量级锁)
volatile ✗(单次读/写除外) ✓(禁止重排) 低(仅内存屏障)
AtomicXxx ✓(CAS) 部分 低~中(CAS + volatile)
ReentrantLock 中等(比 synchronized 略高)
无同步(ThreadLocal) ✓(无共享) 不需要 不需要 最低

实践:正确的双检锁单例

/**
 * 正确的双检锁(Double-Checked Locking)单例实现
 * 使用 volatile 禁止重排序,确保对象完全初始化后才对外可见
 */
public class SafeSingleton {
    // volatile 禁止 "分配内存→赋值给 instance→初始化对象" 的重排
    private static volatile SafeSingleton instance;

    private SafeSingleton() {}

    public static SafeSingleton getInstance() {
        if (instance == null) {                    // 第1次检查:无锁快速路径
            synchronized (SafeSingleton.class) {   // 加锁
                if (instance == null) {              // 第2次检查:防止重复创建
                    instance = new SafeSingleton(); // 现在安全了
                }
            }
        }
        return instance;
    }
}

// 更简洁的替代方案:静态内部类(利用类加载机制的线程安全性)
public class BetterSingleton {
    private BetterSingleton() {}

    private static class Holder {
        static final BetterSingleton INSTANCE = new BetterSingleton();
    }

    public static BetterSingleton getInstance() {
        return Holder.INSTANCE; // JVM 保证类加载线程安全
    }
}
最佳实践

在 Java 中,推荐使用枚举单例(enum Singleton { INSTANCE; })或静态内部类, 它们既线程安全,又防止反序列化重复创建实例,代码也更简洁。 双检锁主要用于需要延迟初始化且初始化成本很高的场景。

本章小结

本章我们建立了 Java 并发编程的理论基础:

下一章我们将进入线程的生命周期、Thread API,以及 Java 21 引入的虚拟线程(Virtual Threads)。