核心结论

volatile不能保证原子性,只能保证单次读/写操作的可见性和有序性,对于复合操作(如i++)无能为力。

原理分析

1. volatile只能保证单个操作的原子性

public class VolatileAtomicityTest {
    private volatile int count = 0;
    
    public void increment() {
        count++;  // 非原子操作!
    }
}

2. 为什么count++不是原子操作?

通过字节码分析:

public void increment();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2   // 读取count的值
       5: iconst_1
       6: iadd               // 加1
       7: putfield      #2   // 写回count
      10: return

分解为三个步骤

  1. 读取:从主内存读取count的当前值到工作内存
  2. 修改:将值加1
  3. 写回:将新值写回主内存

3. 并发问题示例

// 假设count初始值为0,两个线程同时执行count++
// 预期:执行2次后count=2
// 实际:可能count=1

线程A               线程B
读取count=0
                    读取count=0
加1得到1
写回count=1
                    加1得到1
                    写回count=1   覆盖了线程A的结果

问题本质:volatile保证了每次读写的可见性,但无法保证”读-改-写”这个复合操作的原子性。

实战验证

验证代码

public class VolatileAtomicityDemo {
    private volatile int count = 0;
    
    public void increment() {
        count++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileAtomicityDemo demo = new VolatileAtomicityDemo();
        
        // 创建10个线程,每个线程执行1000次increment
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.increment();
                }
            });
            threads[i].start();
        }
        
        // 等待所有线程执行完毕
        for (Thread thread : threads) {
            thread.join();
        }
        
        // 预期:10000,实际:大概率小于10000
        System.out.println("count = " + demo.count);
    }
}

运行结果

count = 9856  // 每次运行结果不同,但大概率小于10000

解决方案对比

方案1:使用synchronized

public class SynchronizedCounter {
    private int count = 0;  // 不需要volatile
    
    public synchronized void increment() {
        count++;  // synchronized保证了原子性
    }
}

方案2:使用AtomicInteger

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // CAS保证原子性,性能更好
    }
}

方案3:使用Lock

public class LockCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

方案4:使用LongAdder(高并发场景)

public class LongAdderCounter {
    private LongAdder count = new LongAdder();
    
    public void increment() {
        count.increment();  // 分段锁思想,性能最优
    }
    
    public long getCount() {
        return count.sum();
    }
}

性能对比

方案 适用场景 性能 优缺点
synchronized 低并发 ★★☆ 简单,但有锁开销
AtomicInteger 中并发 ★★★☆ CAS无锁,但高并发下自旋开销大
ReentrantLock 需要高级特性 ★★☆ 功能强大,代码复杂
LongAdder 高并发累加 ★★★★★ 性能最优,但只能累加

线程安全考量

volatile适用的原子操作

// 以下操作volatile可以保证原子性和可见性
private volatile boolean flag = true;   // 单次写
private volatile int status = 0;        // 单次读写

public void setFlag(boolean newFlag) {
    flag = newFlag;  // OK:单次写操作
}

public boolean getFlag() {
    return flag;     // OK:单次读操作
}

volatile不适用的场景

private volatile int count = 0;

// 错误:复合操作
count++;          // 读-改-写
count += 5;       // 读-改-写

// 错误:依赖当前值的操作
if (count < 10) {
    count = 10;   // 检查-再-操作(check-then-act)
}

答题总结

三句话总结

  1. volatile不保证原子性,因为它只能保证单次读或写的原子性,无法保证复合操作(如count++)的原子性。

  2. 根本原因count++包含”读-改-写”三步,volatile无法保证这三步作为整体的原子性,多线程会发生竞态条件。

  3. 正确做法

    • 简单场景:使用AtomicInteger
    • 复杂场景:使用synchronizedLock
    • 高并发累加:使用LongAdder

面试答题模板

“volatile不能保证原子性。虽然它能保证单次读写的可见性,但像i++这样的复合操作会被编译为’读-改-写’三个步骤,这三个步骤之间可能被其他线程插入,导致数据错误。

举例来说,两个线程同时执行count++时,都可能读到同一个旧值,各自加1后写回,结果只加了1次而不是2次。

解决方案是使用AtomicInteger的CAS操作,或者使用synchronized加锁来保证原子性。”