Skip to content

Volatile关键字为什么不能保证原子性

约 1370 字大约 5 分钟

JVM美团

2025-05-29

⭐ 题目日期:

美团 - 2025/4/25

📝 题解:

volatile 关键字在 Java 中主要用于保证可见性禁止指令重排序,但它不能保证操作的原子性。这是理解并发编程时一个非常关键的区别。

原因在于原子性、可见性和有序性是不同的概念:

  1. 可见性 (volatile 的核心作用之一)

    • 当一个线程修改了一个 volatile 变量的值,这个新值会立即被写回主内存。
    • 当其他线程读取这个 volatile 变量时,它们会强制从主内存中重新加载最新的值,而不是使用自己工作内存(如 CPU 缓存)中的旧值。
    • 这确保了所有线程看到的都是该变量的最新值。
  2. 禁止指令重排序 (volatile 的核心作用之二)

    • 编译器和处理器为了优化性能,可能会对指令的执行顺序进行重排(遵循 as-if-serial 和 happens-before 规则)。
    • 对于 volatile 变量的写操作,任何在该写操作之前发生的读写操作都不能被重排序到该写操作之后。
    • 对于 volatile 变量的读操作,任何在该读操作之后发生的读写操作都不能被重排序到该读操作之前。
    • 这建立了重要的内存屏障,保证了 volatile 变量操作相对于其他内存操作的有序性。
  3. 原子性 (volatile 无法保证)

    • 原子性意味着一个操作是不可分割的整体。要么这个操作完全执行成功,要么它完全不执行。在执行过程中不会被其他线程干扰。
    • 问题在于,很多看似简单的操作(比如 count++)在底层实际上是由多个步骤组成的复合操作
      1. 读取:从内存中读取 count 的当前值到寄存器。
      2. 修改:在寄存器中对值进行加 1 操作。
      3. 写回:将寄存器中的新值写回 count 所在的内存地址。
    • volatile 只能保证上述每个单独的步骤(读、写)本身是原子的。比如,读取一个 volatile int 总是能一次性读到完整的、最近写入的值;写入一个 volatile int 也是一次性完成的。
    • 但是,volatile 无法保证这个“读-改-写”三个步骤组合起来的整体操作是原子的。它无法阻止多个线程在同一个“读-改-写”周期内发生交叉执行

举例说明:为什么 volatile 无法保证 count++ 的原子性

假设 count 是一个 volatile int,初始值为 0。

  1. 线程 A 执行 count++
    • 读取 count (值为 0) -> 修改 (计算 0+1=1) -> 准备写回 1 (但尚未完成写回)
  2. 同时,线程 B 也执行 count++
    • 因为 countvolatile,线程 B 能保证读取到的是最新的主内存值。但是,此时线程 A 还没有将 1 写回主内存,所以线程 B 读到的值仍然是 0
    • 线程 B 读取 count (值为 0) -> 修改 (计算 0+1=1) -> 写回 1 到主内存。
  3. 线程 A 继续执行:将之前计算好的值 1 写回主内存。

最终结果:尽管两个线程都执行了 count++,但最终 count 的值是 1,而不是预期的 2。

发生了什么?

  • volatile 保证了线程 B 读取时能看到线程 A 修改前的值(0),而不是某个中间状态或缓存中的旧值(可见性)。
  • volatile 也保证了线程 A 和线程 B 各自的写操作都是原子的(单个写操作不会被分割)。
  • 但是volatile 无法阻止线程 B 在线程 A 完成整个“读-改-写”序列之前 就读取了 count 的值(此时还是 0)。两个线程都基于 0 进行了加 1 操作,导致结果丢失了一次更新。

如何保证原子性?

如果需要保证像 count++ 这样的复合操作的原子性,需要使用其他机制:

  1. 同步 (synchronized)

    private int count = 0; // 不需要 volatile
    public synchronized void increment() {
        count++;
    }

    synchronized 块或方法提供了互斥性,确保同一时刻只有一个线程能执行 increment() 方法内部的代码(包括读取、修改、写回 count),从而保证了整个复合操作的原子性。它也隐式地提供了可见性和有序性保障。

  2. 原子类 (java.util.concurrent.atomic)

    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // 原子性的 CAS 操作
    }

    AtomicInteger, AtomicLong 等类提供了 incrementAndGet(), getAndAdd(), compareAndSet() 等原子操作方法。它们通常利用底层硬件的比较并交换 (Compare-And-Swap, CAS) 指令来实现无锁的原子操作,性能通常优于 synchronized

总结:

特性volatile 是否保证说明
可见性确保所有线程看到变量的最新值。
有序性 (部分)禁止特定类型的指令重排序,建立 happens-before 关系。
原子性 (复合操作)仅保证对 volatile 变量的单次读或写操作是原子的。无法保证“读-改-写”等复合操作的原子性。

因此,volatile 是解决可见性有序性问题的利器,但它不是解决所有并发问题的万能钥匙。当操作涉及多个步骤(如修改依赖当前值)时,必须使用同步机制(synchronized)或原子类(AtomicXxx)来确保原子性