在 Java 并发编程领域,CAS(Compare-And-Swap)是一种至关重要的原子操作机制,它是许多并发工具和数据结构的底层实现基础。从AtomicInteger到ConcurrentHashMap,从锁机制到线程池,CAS 都扮演着不可或缺的角色。本文将带你从硬件原理到软件实现,全方位剖析 CAS 的工作机制、应用场景、局限性及解决方案,助你真正掌握这一并发编程的核心技术。
CAS,即比较并交换,是一种乐观锁机制,其核心思想是 "先比较,后操作"。与传统的互斥锁(悲观锁)不同,CAS 不需要阻塞线程,而是通过不断重试的方式实现原子操作,从而提高并发性能。
CAS 操作包含三个核心参数:
CAS 的操作过程可以描述为:如果内存地址 V 处的值等于预期值 A,则将该值更新为 B,否则不做任何操作。整个过程是原子的,不会被其他线程中断。
用伪代码表示 CAS 的逻辑:
boolean cas(long address, long expected, long newValue) { if (getMemoryValue(address) == expected) { setMemoryValue(address, newValue); return true; } return false;}
传统的synchronized或Lock机制通过独占资源来保证原子性,会导致未获取锁的线程进入阻塞状态,存在上下文切换的开销。而 CAS 采用非阻塞算法,失败的线程可以立即重试或执行其他操作,避免了线程阻塞的成本。
两种机制的对比:
特性 | 锁机制(悲观锁) | CAS(乐观锁) |
---|---|---|
核心思想 | 假设冲突必然发生,先获取锁再操作 | 假设冲突很少发生,直接操作并检查冲突 |
线程状态 | 未获取锁的线程阻塞挂起 | 失败的线程可以立即重试或做其他事情 |
开销 | 上下文切换、调度成本高 | 无阻塞开销,但可能有重试成本 |
适用场景 | 冲突频繁、临界区大的场景 | 冲突较少、操作简单的场景 |
CAS 并非 Java 独有的特性,而是依赖于硬件平台的原子指令实现。理解其底层实现,能帮助我们更好地掌握其工作原理和性能特性。
现代处理器都提供了支持 CAS 的原子指令,不同架构的 CPU 实现有所不同:
以 x86 的cmpxchg指令为例,其工作流程是:
这些指令在硬件层面保证了操作的原子性,即使在多核心 CPU 环境下也能正确工作。
Java 通过sun.misc.Unsafe类提供了对 CAS 操作的支持,该类包含了一组 native 方法,直接调用底层 CPU 指令:
public final class Unsafe { // 对int类型执行CAS操作 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); // 对long类型执行CAS操作 public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); // 对Object类型执行CAS操作 public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);}
这些方法的参数含义:
JVM 在编译期间会将这些方法映射到底层的 CPU 原子指令,确保操作的原子性。
Java 并发包中的原子类(如AtomicInteger)正是基于Unsafe的 CAS 方法实现的。以AtomicInteger的incrementAndGet方法为例:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // 获取Unsafe实例 private static final Unsafe unsafe = Unsafe.getUnsafe(); // value字段的内存偏移量 private static final long valueOffset; static { try { // 计算value字段的偏移量 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; // 原子自增并返回新值 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }}// Unsafe类中的getAndAddInt实现(简化版)public final int getAndAddInt(Object o, long offset, int delta) { int v; do { // 获取当前值 v = getIntVolatile(o, offset); // 循环执行CAS操作直到成功 } while (!compareAndSwapInt(o, offset, v, v + delta)); return v;}
可以看到,incrementAndGet方法通过自旋 CAS的方式实现原子自增:不断尝试更新直到成功,这也是 CAS 操作的典型使用模式。
CAS 作为底层原子操作,在 Java 并发编程中有广泛的应用,从简单的计数器到复杂的并发容器都能看到其身影。
java.util.concurrent.atomic包提供了一系列基于 CAS 的原子变量类,覆盖了基本类型、引用类型和数组:
这些类提供了原子的更新操作,避免了使用synchronized的开销。例如实现一个线程安全的计数器:
// 线程安全的计数器实现public class ConcurrentCounter { private final AtomicInteger count = new AtomicInteger(0); public int increment() { return count.incrementAndGet(); } public int get() { return count.get(); } public int reset() { return count.getAndSet(0); }}
许多并发容器的实现依赖 CAS 操作来保证线程安全,例如ConcurrentHashMap(JDK 8+):
ConcurrentLinkedQueue则完全基于 CAS 实现了无锁队列:
即使是传统的锁机制,也通过 CAS 进行了优化。例如ReentrantLock的实现:
线程池的状态管理和任务计数也依赖 CAS 操作:
尽管 CAS 有诸多优势,但也存在一些固有的局限性,了解这些局限并掌握应对方案是正确使用 CAS 的关键。
问题描述:当一个变量从 A 被修改为 B,再改回 A 时,CAS 会认为其值没有变化,从而导致错误的更新。这种情况在复杂数据结构中可能引发严重问题,例如链表的节点操作。
典型场景:
解决方案:
// 使用AtomicStampedReference解决ABA问题AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);// 线程1尝试更新int stamp = ref.getStamp(); // 获取当前版本号boolean success = ref.compareAndSet("A", "C", stamp, stamp + 1);// 即使值从A->B->A,版本号也会变化,CAS会失败
问题描述:当并发冲突频繁时,CAS 会不断重试,导致 CPU 长时间占用,造成资源浪费。特别是在单核 CPU 环境下,自旋操作会完全占用 CPU,导致其他线程无法执行。
解决方案:
// 带重试次数限制的CAS实现boolean casWithRetry(AtomicInteger var, int expect, int update, int maxRetries) { int retries = 0; while (retries < maxRetries) { if (var.compareAndSet(expect, update)) { return true; } retries++; // 重试前让出CPU Thread.yield(); } // 超过重试次数,执行其他策略 return false;}
问题描述:CAS 只能对单个变量执行原子操作,无法直接实现多个变量的原子操作(类似于事务的原子性)。
解决方案:
// 将多个变量封装为对象进行CAS操作class Data { int a; int b; // 构造函数和getter/setter省略}AtomicReference<Data> dataRef = new AtomicReference<>(new Data(1, 2));// 原子更新多个变量Data oldData;do { oldData = dataRef.get(); Data newData = new Data(oldData.a + 1, oldData.b * 2);} while (!dataRef.compareAndSet(oldData, newData));
要充分发挥 CAS 的优势,需要了解其性能特性,并在合适的场景中正确使用。
CAS 的性能优势并非绝对,其性能表现受多种因素影响:
性能测试表明:在低冲突场景下,CAS 的性能是synchronized的数倍;但在高冲突场景下,其性能可能不如锁机制。
JDK 提供的原子类已经封装了 CAS 操作,且处理了许多细节问题,应优先使用:
// 推荐AtomicInteger count = new AtomicInteger(0);count.incrementAndGet();// 不推荐(除非有特殊需求)unsafe.compareAndSwapInt(this, valueOffset, expect, update);
CAS 的自旋循环应保持简洁,避免包含耗时操作:
// 不推荐:循环中包含耗时操作while (!casSuccess) { // 耗时操作... casSuccess = atomic.compareAndSet(expect, update);}
CAS 操作的变量需要声明为volatile,以保证内存可见性:
// 正确:value被声明为volatileprivate volatile int value;
CAS 通常作为底层机制,与其他并发工具配合使用能发挥更大价值:
基于 CAS 的思想,研究者设计出了许多高效的非阻塞算法,这些算法构成了现代并发编程的基础。
非阻塞链表通过 CAS 操作实现节点的插入和删除,核心思想是:
Java 中的ConcurrentLinkedQueue就是基于非阻塞链表实现的,其入队和出队操作完全通过 CAS 完成,无需锁机制。
LongAdder是 JDK 8 引入的高性能累加器,针对高并发场景优化:
在高并发场景下,LongAdder的性能远优于AtomicLong,这是对 CAS 冲突问题的巧妙解决。
无锁哈希表通过 CAS 操作实现键值对的插入、删除和查询:
ConcurrentHashMap在 JDK 8 中采用了类似的思想,结合了 CAS 和synchronized,在链表头部使用synchronized,节点操作使用 CAS,兼顾了性能和复杂度。
CAS 作为一种高效的原子操作机制,彻底改变了并发编程的模式。它避免了传统锁机制的阻塞开销,为高并发场景提供了性能保障。从 Java 的原子类到复杂的并发容器,从 JVM 的锁优化到应用层的并发算法,CAS 都扮演着核心角色。
理解 CAS 不仅需要掌握其工作原理,更要深刻认识其适用场景和局限性。在实际开发中,应根据业务特点选择合适的并发策略:简单操作且冲突较少时,CAS 是最佳选择;复杂操作或冲突频繁时,传统锁机制可能更可靠。
随着硬件技术的发展和并发理论的完善,CAS 的实现会不断优化,但它作为无锁编程的核心思想,将继续在并发编程领域发挥重要作用。掌握 CAS,是每个 Java 开发者深入理解并发编程的必经之路。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。