在 Java 内存模型(JMM)中,volatile、锁(synchronized 或显式锁)和 final 关键字都有特定的内存语义,用于控制多线程环境下的可见性、有序性和原子性。以下是它们的核心内存语义及对比:
1. volatile 的内存语义
核心特性
- 可见性:对
volatile变量的写操作,对后续所有读操作可见。 - 有序性:禁止指令重排序优化(通过插入内存屏障)。
内存屏障
- 写操作:在
volatile写之前插入StoreStore屏障,之后插入StoreLoad屏障。 - 读操作:在
volatile读之后插入LoadLoad和LoadStore屏障。
Happens-Before 规则
- 写-读顺序:对
volatile变量的写操作 Happens-Before 后续对该变量的读操作。 - 线程间的传递性:如果线程 A 修改了
volatile变量,线程 B 读取到该值,则线程 A 在写之前的操作对线程 B 可见。
适用场景
- 轻量级的同步机制,适用于单写多读场景(如状态标志位)。
- 不保证原子性(例如
volatile++不是原子操作)。
2. 锁(synchronized 或显式锁)的内存语义
核心特性
- 原子性:锁保证临界区代码的原子执行。
- 可见性:锁的释放会强制将工作内存刷新到主内存,锁的获取会清空本地内存。
- 有序性:临界区内的代码不会被重排序到临界区外。
Happens-Before 规则
- 锁的释放-获取顺序:同一锁的释放操作 Happens-Before 后续对该锁的获取操作。
- 线程间的传递性:线程 A 在释放锁前的操作对线程 B 获取锁后的操作可见。
内存屏障
- 锁释放(Monitor Exit):插入
StoreStore+StoreLoad屏障。 - 锁获取(Monitor Enter):插入
LoadLoad+LoadStore屏障。
适用场景
- 需要保证复合操作的原子性(例如计数器、共享资源修改)。
- 复杂的同步逻辑(如生产者-消费者模型)。
3. final 的内存语义
核心特性
- 不可变性:
final字段一旦初始化后不能修改(引用不可变,对象内部状态可变)。 - 安全发布:构造函数中对
final字段的写入,在对象引用对其他线程可见时,保证final字段的初始化已完成。
内存语义
- 构造函数中的写入:JMM 禁止将
final字段的初始化重排序到构造函数之外。实际上就是构造函数与final的一个间接依赖问题 - 安全发布:通过
final字段可以实现“不可变对象”的安全发布(无需同步)。
Happens-Before 规则
- 构造函数结束 Happens-Before 对象引用被其他线程使用:确保所有线程看到的
final字段是最新值。
示例
public class ImmutableObject {
private final int value; // final 保证安全发布
public ImmutableObject(int value) {
this.value = value; // 初始化完成后对其他线程可见
}
}
对比与总结
| 特性 | volatile | 锁(synchronized) | final |
|---|---|---|---|
| 原子性 | 不保证(单变量写可能原子) | 保证临界区代码原子性 | 不涉及(字段不可变) |
| 可见性 | 写后立即对其他线程可见 | 锁释放后可见 | 对象安全发布后可见 |
| 有序性 | 禁止重排序 | 临界区内代码有序 | 禁止构造函数的初始化重排序 |
| 内存屏障 | 写前 StoreStore + StoreLoad | 锁释放 StoreStore + StoreLoad | 无显式屏障(由 JVM 保证) |
| 适用场景 | 状态标志、单写多读 | 复杂同步逻辑、复合操作 | 不可变对象、安全发布 |
关键组合使用场景
1. 双重检查锁(Double-Checked Locking)
public class Singleton {
private static volatile Singleton instance; // volatile 保证可见性和有序性
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 禁止指令重排序(避免返回未初始化的对象)
}
}
}
return instance;
}
}
volatile防止对象构造时的指令重排序(避免其他线程看到未初始化的对象)。
2. 不可变对象(Immutable Object)
public final class ImmutableData {
private final int x; // final 字段保证安全发布
private final List<String> list; // 引用不可变,但内部状态需自行控制
public ImmutableData(int x, List<String> list) {
this.x = x;
this.list = Collections.unmodifiableList(list); // 防御性复制
}
}
final字段确保对象构造完成后,其他线程看到的是完全初始化的状态。
常见误区
-
volatile不保证原子性:volatile int count = 0; count++;在多线程下仍然不安全(需配合AtomicInteger或锁)。
-
final不保护可变对象内部状态:final List<String> list = new ArrayList<>();的引用不可变,但list.add()仍可能引发线程安全问题。
-
锁的范围过大或过小:
- 过大会降低性能,过小可能导致竞态条件。
通过理解这些内存语义,可以更精准地设计线程安全的高并发程序,避免过度同步或数据竞争问题。