volatile 能保证原子性吗?为什么?

#Java#面试八股

volatile 能保证原子性吗?为什么?

结论先行:不能。volatile 只保证可见性和有序性,对 count++ 这种"读—改—写"复合操作毫无帮助。它能让别的线程立刻看到新值,但拦不住两个线程同时读到同一个旧值。

1. 三条性质,各管一段

性质含义volatile 是否保证
原子性操作不可分割,要么全做要么全不做否(复合操作仍可交错)
可见性一个线程的写,其他线程立刻能读到是(写刷主存、读失效本地副本)
有序性禁止指令重排序是(内存屏障)

2. 丢失更新是怎么发生的

count++ 在字节码层面是四步:getfield → iconst_1 → iadd → putfield。volatile 只保证每一步的读写"看到最新值",不保证这四步整体不被打断。

时刻线程 A线程 B主内存 count
t1读取 count = 00
t2读取 count = 00
t3计算 0+1 = 10
t4计算 0+1 = 10
t5写入 11
t6写入 11(期望 2,实际 1)

两个线程各加一次,结果只加了 1。可见性没问题,原子性丢了。

public class VolatileCounter {
    private static volatile int count = 0;

    public static void main(String[] args) throws Exception {
        Thread[] ts = new Thread[10];
        for (int i = 0; i < 10; i++) {
            ts[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) count++;   // 非原子
            });
            ts[i].start();
        }
        for (Thread t : ts) t.join();
        System.out.println(count);   // 几乎不可能是 10000
    }
}

3. 三种正确的修法

// 方案一:加锁,把"读改写"整体保护起来
private int count;
public synchronized void incr() { count++; }

// 方案二:原子类,靠 CAS 保证"比较并交换"是一个原子指令
private final AtomicInteger count = new AtomicInteger();
public void incr() { count.incrementAndGet(); }

// 方案三:高并发计数用 LongAdder,分散热点(代价是 sum() 非强一致)
private final LongAdder adder = new LongAdder();
public void incr() { adder.increment(); }

压测对比(16 线程、每线程 100 万次 increment,仅作量级参考):

实现相对耗时说明
synchronized最慢每次都要竞争 monitor
AtomicInteger中等单点 CAS,冲突多时自旋
LongAdder最快分段累加,最后求和

4. volatile 到底该用在哪

用法是否合适说明
状态标志 volatile boolean running✅单次写、多方读,天然原子
双重检查锁的 instance✅阻止"半初始化对象"被发布
一次性发布配置对象✅写一次、读多次,配合 final 字段更稳
计数器 volatile int count++❌丢失更新
复合判断 if (flag) { ... } 后改状态❌判断与修改之间可能被插入

一句话:volatile 适合"一个人写、很多人读"的开关类变量,不适合"很多人一起改"的累加类变量。

5. 常见追问

追问一:volatile long 和 long 有什么区别? JLS 17.7 规定:非 volatile 的 long/double 读写可能被拆成两次 32 位操作,理论上读到"半个值";加了 volatile 就保证 64 位读写原子。所以并发环境下共享的 long 变量要么加 volatile,要么加锁。

追问二:volatile 能保证对象内部字段的原子性吗? 不能。volatile 修饰引用只保证引用本身的读写可见且原子,对象里的字段照样可能被并发修改。想安全发布对象,还得让字段 final 或整体不可变。

追问三:为什么 DCL 单例的 instance 必须 volatile? 因为 instance = new Singleton() 包含"分配内存 → 初始化 → 赋引用"三步,重排序可能让引用先被赋值,别的线程拿到一个构造未完成的对象。volatile 禁止这个重排。

追问四:volatile 数组呢? volatile int[] arr 只保证数组引用的可见性,arr[0]++ 完全不受保护。要保护元素,得用 AtomicIntegerArray 或加锁。

小结

volatile 是"广播"而不是"互斥":它负责让所有人立刻知道你改了,但不负责阻止两个人同时改。凡是"读—改—写"复合操作,必须交给锁或原子类。

笔记加载中…