1. 乐观锁与CAS的基本概念
我第一次接触乐观锁是在一个高并发的电商库存系统中。当时系统经常出现超卖问题,传统的悲观锁方案虽然能解决问题,但性能下降严重。直到团队里的架构师提出"为什么不用乐观锁?"这个问题,我才真正开始理解CAS(Compare-And-Swap)这个看似简单却精妙的设计。
乐观锁本质上是一种无锁编程思想,它假设多线程并发访问时不会产生冲突,因此不需要加锁。只有在数据提交更新时,才会检测在此期间是否有其他线程修改过这个数据。如果没有冲突,就执行更新;如果有冲突,则放弃本次操作或重试。这与悲观锁形成鲜明对比——悲观锁总是假设最坏情况,认为每次访问数据都会产生冲突,所以每次都会先加锁。
CAS操作是乐观锁最常见的实现方式,它包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
当且仅当V的值等于A时,处理器才会用B更新V的值,否则不执行更新。整个操作是一个原子操作,由现代CPU直接提供指令支持。在Java中,sun.misc.Unsafe类提供了compareAndSwapInt等native方法,而java.util.concurrent.atomic包下的AtomicInteger等类则是对这些底层操作的封装。
关键理解:CAS不是简单的"先比较再赋值",而是一个不可分割的原子操作。这个原子性是由CPU硬件保证的,不是通过软件锁实现的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CAS的底层硬件支持
2.1 CPU指令级的原子操作
CAS之所以能高效实现,离不开现代CPU的硬件支持。以x86架构为例,它提供了CMPXCHG(Compare and Exchange)指令,这条指令在执行时会被CPU锁定总线或缓存行,确保操作的原子性。我在用JProfiler分析高并发程序时,经常能看到热点代码中大量出现的LOCK CMPXCHG指令。
不同CPU架构的实现方式:
- x86: LOCK CMPXCHG
- ARM: LDREX/STREX指令对
- PowerPC: lwarx/stwcx
这些指令的共同特点是都能在硬件层面实现"比较并交换"的原子语义。Java的CAS操作最终都会通过JNI调用到这些CPU指令。
2.2 缓存一致性协议
CAS的高效性还依赖于现代CPU的缓存一致性协议(如MESI)。在多核环境下,每个CPU核心都有自己的缓存,CAS操作需要保证对某个内存位置的修改对所有核心可见。MESI协议通过维护缓存行的状态(Modified/Exclusive/Shared/Invalid)来实现这一点。
我曾经遇到过一个性能问题:在高频CAS操作下,缓存行频繁失效导致性能下降。通过JOL(Java Object Layout)工具分析对象布局后,发现是因为多个原子变量被放在了同一个缓存行(False Sharing)。解决方案是用@Contended注解或手动填充(padding)来隔离这些变量。
3. Java中的CAS实现
3.1 Unsafe类的魔法
Java中所有CAS操作最终都依赖于sun.misc.Unsafe类(虽然不推荐直接使用)。这个类提供了如下的关键方法:
java复制public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
这些native方法会通过JNI调用到JVM实现中的对应方法。以HotSpot VM为例,其实现代码在unsafe.cpp中,最终会根据平台调用对应的原子指令。
3.2 Atomic类的实现
以AtomicInteger为例,其核心实现如下:
java复制public class AtomicInteger extends Number {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 其他方法...
}
关键点:
- 通过Unsafe获取字段偏移量(valueOffset)
- value字段用volatile修饰保证可见性
- compareAndSet直接委托给Unsafe的CAS操作
3.3 CAS的应用场景
Java并发包中大量使用CAS,例如:
- AtomicXXX类:原子更新基本类型
- ConcurrentHashMap:用于计数器等场景
- AQS(AbstractQueuedSynchronizer):同步框架的基础
- 线程池:控制工作线程数量等
我在实际项目中常用AtomicInteger作为计数器,比synchronized性能高出数倍。但要注意ABA问题(后面会讨论)。
4. CAS的典型问题与解决方案
4.1 ABA问题
ABA问题是指:
- 线程1读取变量值为A
- 线程2将值从A改为B,然后又改回A
- 线程1执行CAS,发现当前值仍是A,认为没有变化,操作成功
这在某些场景下会导致问题,比如链表的头节点被替换过但值没变。解决方案是使用带版本号的原子引用,如AtomicStampedReference。
4.2 自旋开销
当竞争激烈时,线程可能会长时间自旋尝试CAS,消耗CPU资源。JVM对此做了优化:
- 在单核CPU上会直接放弃时间片
- 在多核CPU上会有限次自旋后挂起线程
我在性能调优时发现,对于高竞争场景,有时使用少量自旋+CAS比直接阻塞性能更好,但需要根据具体场景测试。
4.3 只能保证一个变量的原子性
CAS只能保证对一个变量的原子操作,对于多个变量的原子更新,可以使用:
- 将它们封装到一个对象中用AtomicReference
- 使用锁
- 如果变量不多,可以尝试将它们合并到一个long型中用AtomicLong(需位操作)
5. 实际应用案例
5.1 实现无锁栈
以下是一个简单的无锁栈实现:
java复制public class ConcurrentStack<E> {
private AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E item) {
Node<E> newHead = new Node<>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null) return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
private static class Node<E> {
final E item;
Node<E> next;
Node(E item) { this.item = item; }
}
}
这个实现完全不用锁,依靠CAS来保证线程安全。我在一个消息处理系统中使用类似结构,QPS比锁实现提高了3倍。
5.2 计数器实现
比较锁与CAS的性能差异:
java复制// 使用锁的计数器
class LockCounter {
private int value;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
value++;
}
}
}
// 使用CAS的计数器
class CASCounter {
private AtomicInteger value = new AtomicInteger();
public void increment() {
int oldValue;
do {
oldValue = value.get();
} while (!value.compareAndSet(oldValue, oldValue + 1));
}
}
JMH基准测试显示,在低竞争下CAS版本快5-10倍,高竞争下优势减小但仍有2-3倍优势。
6. JVM对CAS的优化
现代JVM对CAS操作做了大量优化:
6.1 偏向锁与CAS
当对象刚被创建时,JVM会启用偏向锁模式。第一个获取锁的线程会通过CAS在对象头中记录自己的线程ID。之后该线程再进入同步块时几乎无开销。只有当其他线程尝试获取锁时,才会撤销偏向锁。
6.2 自适应自旋
JVM会根据历史成功率动态调整自旋次数。如果某个锁最近经常成功获取,JVM会增加自旋次数;反之则减少甚至不自旋。
6.3 锁消除与锁粗化
JVM通过逃逸分析判断某些锁是否必要,如果对象不会逃逸出当前线程,就会消除锁操作。相反,对于连续获取释放同一个锁的操作,JVM可能会合并(粗化)这些操作。
7. 性能调优经验
7.1 减少CAS竞争
在高并发场景下,我常用的几种减少CAS竞争的方法:
- 分散热点:比如将全局计数器拆分为多个段(类似ConcurrentHashMap的分段思想)
- 退避策略:CAS失败后随机等待一小段时间再重试
- 批量处理:积累多个更新后一次性CAS
7.2 伪共享问题
如前所述,多个原子变量位于同一缓存行会导致性能下降。除了用@Contended注解外,还可以通过手动填充来解决:
java复制class PaddedAtomicLong extends AtomicLong {
public volatile long p1, p2, p3, p4, p5, p6 = 7L; // 填充
public PaddedAtomicLong(long initialValue) {
super(initialValue);
}
}
7.3 选择合适的原子类
Java提供了多种原子类:
- AtomicInteger/Long:普通原子变量
- LongAdder:高并发下更好的计数器
- AtomicReference:对象引用
- AtomicStampedReference:带版本号的引用
在极高并发的计数场景下,LongAdder通常比AtomicLong性能更好,因为它采用了分段计数的思想。
