1. 从面试常见误区说起
最近在帮团队面试Java开发时,我发现一个有趣的现象:当问到"什么是CAS"时,80%的候选人都会脱口而出"乐观锁",然后就开始背概念。这让我想起自己刚入行时也是这样,直到有次线上事故让我真正理解了CAS的本质。
那次是在做一个秒杀系统,我自信满满地用了AtomicInteger做库存扣减。结果上线后出现了超卖,排查时才发现自己对CAS的理解只停留在表面。今天我就结合那次踩坑经历,从底层原理到实际应用,带你彻底搞懂这个Java并发编程的核心机制。
2. 为什么需要CAS
2.1 多线程环境下的数据竞争
先看这段简单的计数器代码:
java复制public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这里存在线程安全问题
}
}
当多个线程同时调用increment()时,count++这个看似简单的操作实际上会出问题。因为从CPU角度看,count++包含三个步骤:
- 从内存读取count值到寄存器
- 在寄存器中执行+1操作
- 将新值写回内存
如果线程A和线程B同时执行:
- 线程A读取count=0
- 线程B读取count=0
- 线程A写入1
- 线程B写入1
最终结果count=1而不是预期的2。这就是典型的竞态条件(Race Condition)。
2.2 传统锁方案的局限性
最直接的解决方案是用synchronized:
java复制public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
但synchronized会带来性能问题:
- 线程阻塞导致的上下文切换(每次切换约5-10μs)
- 在竞争不激烈时也会产生不必要的开销
- 容易引发死锁等问题
特别是在高并发场景下,这些开销会被放大。比如我们之前的一个支付系统,使用synchronized后TPS直接从3000降到了800。
3. CAS原理深度解析
3.1 CAS操作的本质
CAS全称Compare-And-Swap,其伪代码如下:
java复制public class SimulatedCAS {
private int value;
public synchronized int compareAndSwap(int expected, int newValue) {
int oldValue = value;
if (oldValue == expected) {
value = newValue;
}
return oldValue;
}
}
关键点在于:
- 这是一个原子操作(由CPU保证)
- 包含三个参数:内存地址V、期望值A、新值B
- 只有当V==A时,才会把V更新为B
在x86架构中,对应的CPU指令是CMPXCHG(Compare and Exchange)。Java通过Unsafe类暴露了这个能力:
java复制public final class Unsafe {
public final native boolean compareAndSwapInt(
Object o, long offset, int expected, int x);
}
3.2 硬件层面的实现
现代CPU通过以下机制实现原子性:
- 总线锁定:LOCK#信号锁定总线,阻止其他CPU访问内存
- 缓存锁定:基于MESI协议,只锁定特定缓存行
以Intel CPU为例,当执行CMPXCHG指令时:
- 检查目标地址是否在缓存行中
- 如果不在,先通过总线读取到缓存
- 锁定该缓存行
- 执行比较和交换
- 释放锁定
这个过程通常只需要几十个时钟周期,远比线程切换高效。
3.3 Java中的CAS实现
以AtomicInteger为例,其incrementAndGet()实现:
java复制public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe中的实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
这就是典型的CAS+自旋模式:
- 读取当前值
- 计算新值
- CAS尝试更新
- 失败则重试
4. CAS在Java并发中的应用
4.1 原子类家族
Java并发包提供了一系列原子类:
- 基本类型:AtomicInteger, AtomicLong, AtomicBoolean
- 引用类型:AtomicReference, AtomicStampedReference
- 数组:AtomicIntegerArray, AtomicLongArray
- 字段更新:AtomicIntegerFieldUpdater
以AtomicLong为例,它的核心实现:
java复制public class AtomicLong {
private volatile long value;
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
}
4.2 ConcurrentHashMap的巧妙设计
JDK8的ConcurrentHashMap大量使用CAS优化:
- 节点插入:通过CAS保证链表头节点原子更新
- 扩容控制:多个线程可以协作完成扩容
- size计算:基于CounterCell的分段计数
关键代码片段:
java复制final V putVal(K key, V value, boolean onlyIfAbsent) {
// 省略部分代码
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
}
4.3 LongAdder的性能优化
在高并发场景下,AtomicLong可能成为瓶颈,因为所有线程都在竞争同一个变量。LongAdder通过分段计数解决了这个问题:
java复制public class LongAdder {
final void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
}
它的核心思想是:
- 初始使用单个base变量
- 竞争激烈时,自动扩展为Cell数组
- 最终结果 = base + ∑cells[i]
5. CAS的局限性与解决方案
5.1 ABA问题
假设如下执行序列:
- 线程1读取V=A
- 线程2修改V=B
- 线程2又修改V=A
- 线程1执行CAS,发现V仍然是A,操作成功
这在某些场景下会有问题,比如栈的实现:
java复制public class Stack {
private AtomicReference<Node> top = new AtomicReference<>();
public void push(Node node) {
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node));
}
}
如果线程1在读取oldTop后暂停,期间其他线程执行了多次pop和push,可能导致栈状态错误。
解决方案是使用AtomicStampedReference或AtomicMarkableReference,它们通过版本号机制解决ABA问题。
5.2 自旋开销
在高竞争环境下,CAS失败率升高会导致CPU空转。JVM对此有几个优化策略:
- 自适应自旋:根据历史成功率动态调整自旋次数
- 延迟:在重试前加入短暂停顿(Thread.yield())
- 退化为锁:当竞争超过阈值时转为阻塞方案
5.3 只能保证单个变量原子性
如果需要保证多个变量的原子性,CAS就无能为力了。这时可以考虑:
- 使用锁
- 把这些变量合并到一个对象中,用AtomicReference来更新
- 使用更高级的并发容器
6. 最佳实践与性能调优
6.1 选择合适的工具
根据场景选择并发工具:
- 低竞争:AtomicInteger/AtomicLong
- 高竞争计数器:LongAdder
- 复杂对象:AtomicReference
- 需要解决ABA问题:AtomicStampedReference
6.2 避免伪共享
CPU缓存以缓存行(通常64字节)为单位。如果多个原子变量在同一个缓存行,会导致性能下降。可以通过@Contended注解(JDK8+)或手动填充来解决:
java复制public class ContendedAtomicLong {
@sun.misc.Contended
private volatile long value;
}
6.3 合理设置自旋次数
对于特定场景,可以通过-XX:PreBlockSpin参数调整自旋次数(默认10次)。但通常建议使用JVM的自适应策略。
7. 真实案例:秒杀系统优化
回到开头提到的秒杀系统问题。最初实现是这样的:
java复制public class SeckillService {
private AtomicInteger stock = new AtomicInteger(1000);
public boolean seckill() {
int current = stock.get();
if (current <= 0) {
return false;
}
return stock.compareAndSet(current, current - 1);
}
}
问题在于:
- 高并发下CAS失败率极高
- 大量线程不断重试导致CPU飙升
- 最终出现超卖(检查与扣减非原子)
优化后的方案:
java复制public class ImprovedSeckillService {
private LongAdder successCount = new LongAdder();
private int totalStock = 1000;
public boolean seckill() {
if (successCount.sum() >= totalStock) {
return false;
}
successCount.increment();
return true;
}
}
这个方案:
- 使用LongAdder处理高并发计数
- 先扣减后校验(允许短暂超卖)
- 配合数据库最终一致性保证
优化后TPS从原来的500提升到了5000+,同时解决了超卖问题。