内存语义

95 阅读4分钟

在 Java 内存模型(JMM)中,volatile、锁(synchronized 或显式锁)和 final 关键字都有特定的内存语义,用于控制多线程环境下的可见性、有序性和原子性。以下是它们的核心内存语义及对比:


1. volatile 的内存语义

核心特性

  • 可见性:对 volatile 变量的写操作,对后续所有读操作可见。
  • 有序性:禁止指令重排序优化(通过插入内存屏障)。

内存屏障

  • 写操作:在 volatile 写之前插入 StoreStore 屏障,之后插入 StoreLoad 屏障。
  • 读操作:在 volatile 读之后插入 LoadLoadLoadStore 屏障。

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锁(synchronizedfinal
原子性不保证(单变量写可能原子)保证临界区代码原子性不涉及(字段不可变)
可见性写后立即对其他线程可见锁释放后可见对象安全发布后可见
有序性禁止重排序临界区内代码有序禁止构造函数的初始化重排序
内存屏障写前 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 字段确保对象构造完成后,其他线程看到的是完全初始化的状态。

常见误区

  1. volatile 不保证原子性

    • volatile int count = 0; count++; 在多线程下仍然不安全(需配合 AtomicInteger 或锁)。
  2. final 不保护可变对象内部状态

    • final List<String> list = new ArrayList<>(); 的引用不可变,但 list.add() 仍可能引发线程安全问题。
  3. 锁的范围过大或过小

    • 过大会降低性能,过小可能导致竞态条件。

通过理解这些内存语义,可以更精准地设计线程安全的高并发程序,避免过度同步或数据竞争问题。