1. CAS(比较并交换)技术概述
CAS(Compare And Swap)是一种原子操作,用于在多线程编程中实现无锁同步。它通过比较内存中的值与预期值来决定是否执行更新操作,整个过程在硬件层面保证原子性。现代CPU通常通过特殊的指令(如x86的CMPXCHG)实现这一机制。
CAS操作包含三个关键参数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
当且仅当V的值等于A时,处理器才会将V的值更新为B,否则不执行任何操作。无论哪种情况,都会返回V的原始值。
2. CAS底层实现原理
2.1 硬件支持
现代处理器通常提供以下原子指令支持CAS:
- x86架构:CMPXCHG指令
- ARM架构:LDREX/STREX指令对
- PowerPC架构:lwarx/stwcx指令对
这些指令通过总线锁定或缓存一致性协议(如MESI)保证操作的原子性。以x86为例,CMPXCHG指令执行时会自动锁定总线或缓存行,防止其他核心同时修改同一内存位置。
2.2 Java中的实现
Java通过sun.misc.Unsafe类提供CAS操作,典型方法包括:
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);JVM会根据不同平台将这些方法映射到对应的CPU指令。例如在x86平台,compareAndSwapInt会编译为包含lock cmpxchg的机器码。
3. CAS的典型应用场景
3.1 原子类实现
Java并发包中的原子类(如AtomicInteger)基于CAS实现:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 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; }3.2 锁优化
CAS是构建轻量级锁(如自旋锁)的基础:
class SpinLock { private AtomicBoolean locked = new AtomicBoolean(false); public void lock() { while (!locked.compareAndSet(false, true)) { // 自旋等待 } } public void unlock() { locked.set(false); } }3.3 并发容器
ConcurrentHashMap等并发容器使用CAS实现无锁化的节点插入和更新:
final V putVal(K key, V value, boolean onlyIfAbsent) { // ... for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // CAS成功插入新节点 } // ... } }4. CAS的局限性及解决方案
4.1 ABA问题
问题描述:线程1读取变量值为A,线程2将值改为B后又改回A,此时线程1的CAS操作仍会成功,可能导致逻辑错误。
解决方案:
- 版本号机制:AtomicStampedReference
- 时间戳标记:每次修改都更新时间戳
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0); // 更新时检查版本号 int[] stampHolder = new int[1]; int current = atomicRef.get(stampHolder); if (atomicRef.compareAndSet(current, 200, stampHolder[0], stampHolder[0]+1)) { // 更新成功 }4.2 循环开销
高竞争环境下,CAS失败会导致大量CPU空转。解决方案:
- 指数退避策略:失败后逐渐增加等待时间
- 自适应自旋:根据历史成功率动态调整自旋次数
4.3 单一变量限制
CAS只能保证单个变量的原子操作。对于复合操作,需要:
- 使用锁机制
- 采用原子引用(AtomicReference)封装多个变量
5. CAS性能优化实践
5.1 伪共享避免
通过填充(padding)防止多个原子变量位于同一缓存行:
@sun.misc.Contended class PaddedAtomicLong extends AtomicLong { // 自动填充缓存行 }5.2 批量操作
对于计数器等场景,可采用LongAdder替代AtomicLong:
LongAdder counter = new LongAdder(); counter.increment(); // 内部使用Cell数组分散竞争5.3 内存屏障控制
根据场景选择合适的内存语义:
// 弱化内存屏障提升性能 AtomicInteger relaxed = new AtomicInteger(); relaxed.lazySet(1); // 不保证立即可见性6. CAS与其他同步机制对比
| 特性 | CAS | 互斥锁 | volatile |
|---|---|---|---|
| 原子性 | 单操作原子 | 代码块原子 | 单操作非原子 |
| 阻塞性 | 非阻塞 | 阻塞 | 非阻塞 |
| 适用场景 | 简单原子更新 | 复杂同步逻辑 | 状态可见性 |
| 性能特点 | 低竞争时高效 | 高竞争时稳定 | 读写开销小 |
| 内存语义 | 强一致性 | 取决于实现 | 可见性保证 |
7. 典型问题排查
7.1 CAS失败率过高
排查要点:
- 检查竞争线程数量
- 分析操作耗时(是否包含复杂计算)
- 考虑采用分段策略
7.2 可见性问题
即使CAS成功,后续读取可能看到旧值。解决方案:
AtomicInteger ai = new AtomicInteger(); ai.setRelease(newValue); // 保证后续读取可见7.3 平台兼容性
某些ARM处理器需要显式内存屏障:
// 在ARM上需要额外屏障 UNSAFE.storeFence(); boolean success = compareAndSwapInt(...);8. 最佳实践建议
- 简单计数场景优先使用LongAdder
- 对象引用更新使用AtomicReference
- 需要版本控制的场景使用AtomicStampedReference
- 高竞争时考虑退避策略或锁升级
- 关键路径避免不必要的CAS操作
注意事项:在JDK9+中,Unsafe的CAS方法已被VarHandle替代,新代码应优先使用VarHandle API:
VarHandle handle = MethodHandles.lookup().findVarHandle(...); handle.compareAndSet(obj, expected, newValue);CAS作为并发编程的基础构建块,理解其原理和适用场景对于编写高性能并发代码至关重要。在实际应用中,需要根据具体场景在原子性、可见性和性能之间做出权衡。