核心结论
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
分解为三个步骤:
- 读取:从主内存读取count的当前值到工作内存
- 修改:将值加1
- 写回:将新值写回主内存
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)
}
答题总结
三句话总结
-
volatile不保证原子性,因为它只能保证单次读或写的原子性,无法保证复合操作(如
count++)的原子性。 -
根本原因:
count++包含”读-改-写”三步,volatile无法保证这三步作为整体的原子性,多线程会发生竞态条件。 -
正确做法:
- 简单场景:使用
AtomicInteger - 复杂场景:使用
synchronized或Lock - 高并发累加:使用
LongAdder
- 简单场景:使用
面试答题模板
“volatile不能保证原子性。虽然它能保证单次读写的可见性,但像
i++这样的复合操作会被编译为’读-改-写’三个步骤,这三个步骤之间可能被其他线程插入,导致数据错误。举例来说,两个线程同时执行
count++时,都可能读到同一个旧值,各自加1后写回,结果只加了1次而不是2次。解决方案是使用
AtomicInteger的CAS操作,或者使用synchronized加锁来保证原子性。”