volatile关键字的作用是什么?它能保证原子性吗?

3 阅读4分钟

volatile 是 Java 中一个轻量级的同步关键字,主要用于修饰变量。它的核心作用是保证可见性禁止指令重排序,但不能保证原子性

一、volatile 的两大作用

1. 保证可见性

  • 问题背景:在 Java 内存模型(JMM)中,每个线程有自己的工作内存(缓存),主内存中的变量可能被线程缓存到本地。当一个线程修改了共享变量后,其他线程无法立刻看到这个修改,导致数据不一致。
  • volatile 解决方案
    volatile 修饰的变量,写操作会立即刷新到主内存;读操作会直接从主内存读取(跳过本地缓存)。
    这样,一个线程修改了 volatile 变量,其他线程能马上看到最新值。
// 示例:volatile 保证可见性,用于线程停止标志
class Runner {
    private volatile boolean running = true;  // 不加 volatile,子线程可能永远看不到修改

    public void stop() { running = false; }

    public void run() {
        while (running) {
            // 执行任务
        }
    }
}

2. 禁止指令重排序(保证有序性)

  • 问题背景:JVM 和 CPU 为了优化性能,可能会对指令进行重排序(在不改变单线程执行结果的前提下)。在多线程环境下,重排序可能导致意想不到的错误。
  • volatile 解决方案
    volatile 变量的读写操作前后插入内存屏障(Memory Barrier),禁止编译器 / CPU 对相关指令重新排序。
    典型应用:双重检查锁定(Double-Checked Locking)单例模式
// 错误的单例(不加 volatile,可能返回未完全初始化的对象)
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(); // 实际分三步:1.分配内存 2.初始化 3.引用指向内存
                }   // volatile 禁止第 2 和 3 步的重排序,防止其他线程看到空引用但未初始化的对象
            }
        }
        return instance;
    }
}

二、volatile 不能保证原子性

为什么不能保证原子性?

原子性是指一个或多个操作要么全部执行且不被中断,要么全不执行。
volatile 只能保证单个读 / 写操作的原子性(例如读一个 long 或写一个 long 是原子的),但复合操作(如 i++)不是原子的:
i++ 包含“读 - 改 - 写”三步,在多线程下即使 ivolatile,也会发生丢失更新的问题。

volatile int count = 0;

// 两个线程各执行 10000 次 count++,最终结果可能小于 20000
void increment() {
    count++;  // 1.读 count 到寄存器 2.加1 3.写回内存 —— 不是原子操作
}

如何实现原子操作?

  • 使用 synchronizedReentrantLock 对整个复合操作加锁。
  • 使用 java.util.concurrent.atomic 包下的原子类,如 AtomicInteger(基于 CAS 实现,保证原子性)。
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();  // 原子自增

三、volatilesynchronized 对比

特性volatilesynchronized
作用保证可见性、有序性(禁止重排序)保证原子性、可见性、有序性(通过锁的互斥)
是否阻塞不阻塞线程阻塞其他竞争锁的线程
适用对象只能修饰变量修饰方法、代码块
原子性保证不能保证复合操作的原子性保证代码块内的操作原子执行
性能比加锁轻量,读写开销小较重(但现代 JVM 已优化)

四、典型使用场景

1. 状态标志(开关)

volatile boolean shutdownRequested = false;
// 线程1: shutdownRequested = true;
// 线程2: while (!shutdownRequested) { ... }

2. 双重检查锁定的单例模式(如前述)

3. 读操作远多于写操作的“一写多读”变量

例如统计配置参数、温度传感器数值等,只有一个线程更新,多个线程读取,用 volatile 即可保证可见性,无需加锁。

总结

  • volatile 的作用:保证变量在多线程间的可见性以及禁止指令重排序
  • 不能保证原子性:对于 ++-- 或其他复合操作,仍需加锁或使用原子类。
  • 选择建议:如果只是需要某个变量的修改对其他线程立即可见,且操作本身是原子的(例如赋值 a = 1,或者 boolean 标志位),用 volatile;如果需要复合操作的原子性,用 synchronizedLock