理解 Java 并发的内存语义,绕不开 happens-before 与 JMM(Java Memory Model)。本文围绕 synchronized、volatile、final 三个关键字,从「happens-before → 各关键字的语义与底层实现 → 三者对比」的脉络展开。

一、happens-before

happens-before 是 JMM 中一套跨线程的可见性规则。它不要求 A 线程一定在 B 线程前执行,而是保证:

如果 A happens-before B,那么 A 的执行结果对 B 可见。

JVM 中的核心规则(部分)

  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. start() 规则:线程 A 调用 B.start() happens-before 线程 B 中的一切操作。
  6. join() 规则:线程 B 中的所有操作 happens-before 线程 A 从 B.join() 成功返回。
  7. final 字段规则:构造函数中对 final 字段的写 happens-before 后续对该对象引用的读(有前提)。

二、synchronized

保证方法、代码块的:

  • 原子性:临界区代码同一时刻只被一个线程执行。
  • 有序性:临界区内代码不会被重排序到锁外。
  • 可见性:释放锁前的所有写操作,对后续获得同一把锁的线程可见。

底层

  • 修饰实例方法 → 锁 this;修饰静态方法 → 锁 Class 对象;修饰代码块 → 锁括号里的对象。
  • 依赖对象头 Mark Word + Monitor(重量级锁),JDK 6 之后有偏向锁、轻量级锁、自旋优化。

用途

  • 复合操作的原子性,如 i++、检查再执行(check-then-act)、单例双重检查锁。
  • 需要"一组操作整体互斥"的场景。

三、volatile

语义

  • 可见性:写 volatile 变量后立即刷新回主存,读 volatile 变量从主存中读取。
  • 有序性:禁止指令重排,插入内存屏障:
    • 写前插 StoreStore,写后插 StoreLoad;
    • 读后插 LoadLoad、LoadStore。
  • 不保证原子性:对 volatile 变量的 i++ 不是原子操作。

底层

通过 lock 前缀指令 / 内存屏障实现,开销小于锁。

用途

  1. 状态标志位:volatile boolean running = true;,一个线程写,另一个线程读。
  2. 双重检查锁的单例:private volatile static Singleton instance;,防止"半初始化对象"逸出。
  3. 一写多读的共享变量。

四、final

语义

  • 不可变性:一旦赋值后不允许再修改(引用不可变,但对象内部状态可变——除非对象本身不可变)。
  • 可见性(final 字段规则):在正确构造(构造函数中没有 this 逸出)的前提下,构造函数中对 final 字段的写 happens-before 后续任何线程对该对象引用的读。也就是说,其他线程看到对象引用时,一定能看到 final 字段已正确初始化,无需同步。

这是 JMM 对 final 的特殊保证,普通字段没有这个待遇。

底层

编译器会在 final 字段写之后、构造函数返回之前插入 StoreStore 屏障。

典型用途

  • 不可变对象(配合 private、不提供 setter)。
  • 安全发布对象,避免"半初始化"问题。
  • 常量定义。

五、三者对比

维度synchronizedvolatilefinal
原子性✅ 保证❌ 不保证—(本身不可修改)
可见性✅✅✅(正确构造时)
有序性✅(临界区内)✅(禁止重排)构造期保证
是否阻塞阻塞不阻塞—
作用对象方法 / 代码块变量变量 / 方法 / 类
性能较重(有优化)轻无运行时开销
建立 happens-beforeunlock → lock写 → 读构造写 → 引用读