CAS (Compare-And-Swap) 详解
CAS(比较并交换)是一个常见的无锁同步机制,它通过原子操作来保证线程间对共享变量的操作一致性。在 Java 中,CAS 被广泛应用于实现高性能的并发控制,尤其是在无锁的原子类中。下面我们将深入了解 Java 中 CAS 的实现原理、相关机制以及常见问题。
1. CAS 的基本原理
CAS 的核心思想是,通过比较当前变量的值是否等于预期值,如果相等则更新为新值。CAS 操作是原子性的,这意味着它是不可中断的,要么成功更新,要么失败重试。
CAS 操作流程:
- 读取共享变量的当前值。
- 比较当前值和期望值(预期值),如果相等,表示没有其他线程修改该变量,则将其更新为新值。
- 如果当前值和期望值不相等,表示在此期间其他线程已经修改了变量,CAS 操作失败,线程会放弃更新,通常会重新读取当前值并再次尝试。
CAS 的实现依赖于 CPU 的硬件支持,通常是通过原子指令(如 CMPXCHG
指令)来实现,保证了操作的原子性。
2. CAS 在 Java 中的实现
在 Java 中,CAS 的实现通常依赖于 sun.misc.Unsafe
类。Unsafe
提供了直接操作内存和执行底层原子操作的能力。
Unsafe
类中的 CAS 方法:
Unsafe
类的 compareAndSwap
方法有不同的变体,分别用于操作 Object
、int
和 long
类型。以下是一些常用方法的签名:
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
这些方法的作用是比较 o
对象中指定偏移量的值与期望值(expected
)是否相同,如果相同,则将该值更新为新的值(x
)。这些方法底层会通过原子指令进行实现,从而确保更新操作的原子性。
Unsafe
中的 CAS 示例:
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); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这段代码展示了如何通过 Unsafe
获取 AtomicInteger
类中 value
字段的内存偏移量,并通过 compareAndSwapInt
方法进行原子操作。这里的 compareAndSet
方法就通过 CAS 来保证 value
值的更新。
3. CAS 的优势和问题
优势:
- 无锁操作:CAS 无需加锁,避免了传统锁带来的上下文切换和性能开销,特别适用于高并发场景。
- 原子性:CAS 保证了操作的原子性,多个线程并发时,只有一个线程能够成功更新共享变量。
问题:
尽管 CAS 提供了高效的并发控制机制,但它也存在一些问题,最常见的就是 ABA 问题。
3.1. ABA 问题
CAS 操作的一个关键问题是 ABA 问题。ABA 问题指的是,线程 A 在执行 CAS 操作时,发现某个变量的值与预期值相等,但该值在操作过程中被线程 B 修改过一次,然后再改回原值。此时,CAS 会误认为该变量从未被修改过,导致错误的更新。
举个例子:
- 线程 A 读取某个变量,假设其值为
A
。 - 线程 B 修改该变量的值为
B
,然后再将其修改回A
。 - 线程 A 通过 CAS 比较后,认为该变量的值仍然是
A
,于是更新为新的值。
这种情况可能导致数据不一致。
解决方案:
为了解决 ABA 问题,Java 提供了 AtomicStampedReference
类,它通过引入一个版本号(或时间戳)来标记每次修改,从而避免了 ABA 问题。在 CAS 操作时,不仅要比较值是否相等,还要比较版本号是否一致。
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference && newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
3.2. 自旋开销
CAS 操作往往伴随着自旋(即在操作失败时不断重试),如果并发冲突很频繁,CAS 会不断地失败并重试,这时会占用大量 CPU 资源,造成性能开销。为了减少自旋的成本,部分实现会引入休眠或暂停指令,如 pause
指令。
3.3. 只能保证单个变量的原子操作
CAS 操作通常是对单个变量进行原子更新。如果需要对多个变量进行操作,CAS 无法保证其原子性。为了解决这个问题,可以将多个变量封装成一个对象,通过 AtomicReference
等类来实现对复合对象的 CAS 操作。
总结
CAS(Compare-And-Swap)是一种无锁的同步机制,通过原子操作保证了线程安全。在 Java 中,CAS 通过 Unsafe
类实现,底层通过 CPU 的原子指令来完成操作。尽管 CAS 提供了高效的并发控制,但也存在一些问题,如 ABA 问题和自旋开销。为了应对这些问题,Java 提供了如 AtomicStampedReference
等类,帮助开发者更好地利用 CAS 进行高效的并发控制。