volatile 能保证原子性吗?为什么?
结论先行:不能。volatile 只保证可见性和有序性,对 count++ 这种"读—改—写"复合操作毫无帮助。它能让别的线程立刻看到新值,但拦不住两个线程同时读到同一个旧值。
1. 三条性质,各管一段
| 性质 | 含义 | volatile 是否保证 |
|---|---|---|
| 原子性 | 操作不可分割,要么全做要么全不做 | 否(复合操作仍可交错) |
| 可见性 | 一个线程的写,其他线程立刻能读到 | 是(写刷主存、读失效本地副本) |
| 有序性 | 禁止指令重排序 | 是(内存屏障) |
2. 丢失更新是怎么发生的
count++ 在字节码层面是四步:getfield → iconst_1 → iadd → putfield。volatile 只保证每一步的读写"看到最新值",不保证这四步整体不被打断。
| 时刻 | 线程 A | 线程 B | 主内存 count |
|---|---|---|---|
| t1 | 读取 count = 0 | 0 | |
| t2 | 读取 count = 0 | 0 | |
| t3 | 计算 0+1 = 1 | 0 | |
| t4 | 计算 0+1 = 1 | 0 | |
| t5 | 写入 1 | 1 | |
| t6 | 写入 1 | 1(期望 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 是"广播"而不是"互斥":它负责让所有人立刻知道你改了,但不负责阻止两个人同时改。凡是"读—改—写"复合操作,必须交给锁或原子类。