理解 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 中的核心规则(部分)
- 程序顺序规则:同一线程内,前面的操作 happens-before 后面的操作。
- 监视器锁规则:对同一个锁的
unlockhappens-before 后续对该锁的lock。 - volatile 规则:对
volatile变量的写 happens-before 后续对该变量的读。 - 传递性:若 A happens-before B,B happens-before C,则 A happens-before C。
- start() 规则:线程 A 调用
B.start()happens-before 线程 B 中的一切操作。 - join() 规则:线程 B 中的所有操作 happens-before 线程 A 从
B.join()成功返回。 - 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 前缀指令 / 内存屏障实现,开销小于锁。
用途
- 状态标志位:
volatile boolean running = true;,一个线程写,另一个线程读。 - 双重检查锁的单例:
private volatile static Singleton instance;,防止"半初始化对象"逸出。 - 一写多读的共享变量。
四、final
语义
- 不可变性:一旦赋值后不允许再修改(引用不可变,但对象内部状态可变——除非对象本身不可变)。
- 可见性(final 字段规则):在正确构造(构造函数中没有
this逸出)的前提下,构造函数中对final字段的写 happens-before 后续任何线程对该对象引用的读。也就是说,其他线程看到对象引用时,一定能看到final字段已正确初始化,无需同步。
这是 JMM 对 final 的特殊保证,普通字段没有这个待遇。
底层
编译器会在 final 字段写之后、构造函数返回之前插入 StoreStore 屏障。
典型用途
- 不可变对象(配合
private、不提供 setter)。 - 安全发布对象,避免"半初始化"问题。
- 常量定义。
五、三者对比
| 维度 | synchronized | volatile | final |
|---|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 | —(本身不可修改) |
| 可见性 | ✅ | ✅ | ✅(正确构造时) |
| 有序性 | ✅(临界区内) | ✅(禁止重排) | 构造期保证 |
| 是否阻塞 | 阻塞 | 不阻塞 | — |
| 作用对象 | 方法 / 代码块 | 变量 | 变量 / 方法 / 类 |
| 性能 | 较重(有优化) | 轻 | 无运行时开销 |
| 建立 happens-before | unlock → lock | 写 → 读 | 构造写 → 引用读 |