多线程中的 CAS
CAS(Compare-And-Swap 或 Compare-And-Set) 是一种硬件层面的原子操作,用于解决多线程并发中的数据一致性问题。它允许在没有锁的情况下进行并发编程,是许多非阻塞算法(如无锁队列、无锁栈)的核心基础。
CAS 操作的基本思想是:比较并交换。即在更新某个变量时,先检查它的值是否符合预期,如果符合预期则进行更新,否则不进行更新。
CAS 是一种高效的、无锁的并发操作,它通过硬件支持的原子操作来避免锁竞争,适合用于并发环境中的共享数据更新。CAS 操作能够极大地提升多线程性能,但也有一些缺点,比如 ABA 问题和自旋可能带来的性能问题。在 Java 中,CAS 机制广泛应用于 java.util.concurrent.atomic
包中的类,帮助我们更高效地管理并发操作。
CAS 的工作原理
CAS 操作包含三个操作数:
内存中的值(V):当前变量在内存中的值。
期望值(E):我们期望该变量现在应该是的值。
新值(N):如果当前变量的值等于期望值,那么我们想把它更新为的新值。
CAS 操作执行的步骤如下:
检查内存中变量 V
的当前值是否等于 E
(期望值)。如果等于 E
,则将 V
更新为 N
(新值)。如果不等于 E
,则说明其他线程已经修改了该变量,CAS 操作失败,返回当前的 V
值。
CAS 是一种原子操作,在硬件级别上提供保障,因此多个线程可以同时进行比较并交换操作,而不需要锁来保证同步。
CAS 的应用
CAS 通常用于解决多线程环境下的共享资源竞争问题,避免使用锁机制带来的开销。Java 中提供了对 CAS 的直接支持,特别是在 java.util.concurrent.atomic
包中,例如 AtomicInteger
、AtomicBoolean
、AtomicReference
等都使用了 CAS。
例如,AtomicInteger
类的 compareAndSet()
方法就是一个典型的 CAS 操作:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// 期望值为5,如果atomicInteger的当前值为5,则更新为10
boolean isUpdated = atomicInteger.compareAndSet(5, 10);
System.out.println("CAS 更新成功: " + isUpdated);
System.out.println("当前值: " + atomicInteger.get());
}
}
运行结果:
CAS 更新成功: true 当前值: 10
在上面的代码中,atomicInteger.compareAndSet(5, 10)
检查当前的值是否是5,如果是,则将其更新为10。由于初始值是5,因此操作成功,返回 true
。如果另一个线程在此操作之前修改了 atomicInteger
的值,则 compareAndSet
会返回 false
,而不会更新值。
CAS 的优点
无锁操作:CAS 是一种非阻塞算法,通过避免锁来提高并发性能,减少线程之间的竞争等待时间,适合高并发环境。
性能高效:CAS 操作是硬件层面的原子操作,因此比传统的加锁机制效率高,特别是在多线程争用激烈时,锁的开销会很大,而 CAS 通过自旋等待避免了锁的开销。
CAS 的缺点
虽然 CAS 有很多优势,但它也存在一些缺点:
ABA 问题:
Java 提供了 AtomicStampedReference
来解决 ABA 问题,通过引入版本号或时间戳,记录每次修改的时间戳,防止 ABA 问题的出现。
示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAProblemTest {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 1);
int[] stampHolder = new int[1];
Integer ref = atomicStampedRef.get(stampHolder);
System.out.println("初始值: " + ref + ", 初始版本: " + stampHolder[0]);
// 修改值为101
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
// 再次修改回100
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
// 尝试使用原来的版本号来更新值
boolean result = atomicStampedRef.compareAndSet(100, 101, 1, 2); // 版本号不匹配,更新失败
System.out.println("更新成功: " + result);
}
}
ABA 问题是指:一个变量的值从 A
变成 B
,然后又变回 A
。对于 CAS 操作,它只会检查当前值是否与预期值相同,但不关心该值在此过程中是否发生了变化。这可能导致错误的判断,认为变量的值没有被修改。
自旋导致 CPU 资源浪费:
CAS 操作是通过不断重试(自旋)来尝试更新的。如果多个线程频繁竞争同一个变量的更新,可能导致大量的自旋操作,从而消耗大量的 CPU 资源。
只能对单个变量操作:
CAS 只能保证单个变量的原子性操作,无法直接保证多个变量的原子性。如果要操作多个共享变量,就需要借助锁或其他同步机制。