volatile 与 CAS
volatile 和 CAS 是 Java 并发编程中实现线程安全的轻量级机制,相比 synchronized 具有更低的性能开销。
volatile 关键字
三大特性
volatile 保证的是可见性和有序性,不保证原子性。
| 特性 | 说明 | 原理 |
|---|---|---|
| 可见性 | 一个线程修改后,其他线程立即可见 | 内存屏障 + 缓存一致性协议 |
| 有序性 | 禁止指令重排序 | 内存屏障 |
| 原子性 | ❌ 不保证 | - |
可见性示例
public class VisibilityDemo {
// 不加 volatile,线程可能不会看到 running 的变化
private volatile boolean running = true;
public void stop() {
running = false;
}
public void doWork() {
while (running) {
// 工作逻辑
}
System.out.println("停止工作");
}
}原理:
- 写入 volatile 变量时,强制刷新到主内存
- 其他线程的工作内存中的副本失效
- 读取时必须从主内存重新加载
禁止重排序
volatile 通过内存屏障(Memory Barrier)防止指令重排序:
// 经典的单例双重检查锁定
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}为什么需要 volatile?
new Singleton() 分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存
如果没有 volatile,步骤 2 和 3 可能重排序,导致其他线程看到未初始化的对象。
内存屏障
JVM 在 volatile 变量操作前后插入内存屏障:
写操作:
StoreStore屏障 → volatile写 → StoreLoad屏障
读操作:
LoadLoad屏障 → volatile读 → LoadStore屏障不适用场景
volatile 不适合复合操作:
// ❌ 错误:volatile 不能保证原子性
private volatile int count = 0;
public void increment() {
count++; // 读取-修改-写入,不是原子操作
}
// ✅ 正确:使用 Atomic 类
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}CAS(Compare-And-Swap)
原理
CAS 是一种乐观锁机制,操作包含三个操作数:
- V:内存值
- E:预期值
- N:新值
当且仅当 V == E 时,将 V 设置为 N,否则不做任何操作。
// CAS 伪代码
boolean cas(int[] memory, int expected, int newValue) {
if (memory[0] == expected) {
memory[0] = newValue;
return true;
}
return false;
}Java 中的实现
CAS 通过 Unsafe 类的本地方法实现:
public class AtomicInteger {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}底层依赖 CPU 的 cmpxchg 指令,是原子操作。
Atomic 原子类
Java 提供了一系列原子类,基于 CAS 实现:
| 类名 | 说明 |
|---|---|
| AtomicInteger | 整数原子类 |
| AtomicLong | 长整型原子类 |
| AtomicBoolean | 布尔原子类 |
| AtomicReference | 引用类型原子类 |
| AtomicIntegerArray | 整数数组原子类 |
| LongAdder | 高性能累加器 |
AtomicInteger count = new AtomicInteger(0);
// 自增
count.incrementAndGet(); // i++
// 自减
count.decrementAndGet(); // i--
// CAS 操作
count.compareAndSet(0, 1); // 如果当前值是0,设置为1
// 获取并更新
count.getAndUpdate(x -> x * 2); // 原子地乘以2ABA 问题
CAS 只检查值是否相等,无法检测值是否被修改过又改回来。
示例:
初始值: A
线程1: 读取 A,准备改为 C
线程2: A → B → A(修改后又改回来)
线程1: CAS(A, C) 成功,但中间状态被忽略解决方案:添加版本号
// AtomicStampedReference 解决 ABA 问题
AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(100, 0); // 初始值100,版本号0
int[] stampHolder = new int[1];
Integer value = ref.get(stampHolder); // 获取值和版本号
// CAS 时同时检查值和版本号
ref.compareAndSet(value, 101, stampHolder[0], stampHolder[0] + 1);CAS vs synchronized
| 对比项 | CAS | synchronized |
|---|---|---|
| 锁类型 | 乐观锁 | 悲观锁 |
| 阻塞 | 不阻塞,自旋重试 | 阻塞等待 |
| 性能 | 低竞争时性能好 | 高竞争时更稳定 |
| CPU 开销 | 自旋消耗 CPU | 上下文切换开销 |
| 适用场景 | 简单操作 | 复杂临界区 |
LongAdder 高性能累加
JDK 8 引入 LongAdder,在高并发累加场景下性能优于 AtomicLong。
原理:分散热点,使用多个 Cell 存储,最终求和。
LongAdder adder = new LongAdder();
// 累加(无返回值,性能更高)
adder.increment();
adder.add(10);
// 获取总和
long sum = adder.sum();选择:
- 需要获取中间结果 → AtomicLong
- 仅需最终结果 → LongAdder
常见面试题
Q1: volatile 能保证线程安全吗?
不能完全保证。volatile 只保证可见性和有序性,不保证原子性。对于简单的标志位场景可以保证线程安全,但对于复合操作(如 count++)不能保证。
Q2: CAS 有什么缺点?
- ABA 问题:可通过版本号解决
- 循环时间长开销大:自旋失败时持续消耗 CPU
- 只能保证一个共享变量的原子操作:多个变量需要加锁
Q3: 为什么 Atomic 类比 synchronized 性能好?
- Atomic 基于 CAS 实现,不阻塞线程
- synchronized 会阻塞线程,涉及上下文切换
- 低竞争时 CAS 性能更好
Q4: i++ 是线程安全的吗?如何保证?
// ❌ 不安全
int i = 0;
i++;
// ✅ 方式一:synchronized
synchronized(lock) { i++; }
// ✅ 方式二:AtomicInteger
AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();
// ✅ 方式三:LongAdder(高并发累加)
LongAdder i = new LongAdder();
i.increment();小结
- volatile 保证可见性和有序性,不保证原子性
- volatile 适用于状态标志、单例双重检查锁定
- CAS 是乐观锁,基于 CPU 原子指令
- Atomic 类基于 CAS 实现,适合简单原子操作
- ABA 问题可用 AtomicStampedReference 解决
- LongAdder 适合高并发累加场景