什么是 CAS
CAS 即 Compare-And-Swap(比较并交换),它是一种无锁算法,用于在多线程环境下实现同步机制。在硬件层面,许多处理器都提供了 CAS 指令,Java 借助这些底层指令来实现并发操作。
基本原理
CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置 V 的值等于预期原值 A 时,处理器才会用新值 B 更新内存位置 V 的值;否则,它不会执行更新操作,但会返回 V 的当前值。整个比较并交换的操作是原子性的,由硬件保证其不可分割。
Java 中的实现
在 Java 中,java.util.concurrent.atomic 包下提供了一系列基于 CAS 实现的原子类,例如 AtomicInteger、AtomicLong 等。以下是一个简单的 AtomicInteger 使用示例:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
// 尝试将值从 10 改为 20
boolean result = atomicInteger.compareAndSet(10, 20);
System.out.println("更新结果: " + result);
System.out.println("当前值: " + atomicInteger.get());
}
}
在上述代码中,compareAndSet 方法就是一个 CAS 操作,它会先比较当前值是否为 10,如果是则将其更新为 20,并返回 true;否则返回 false。
CAS 产生的问题及解决方案
- ABA 问题
● 问题描述:ABA 问题是指一个值从 A 变为 B,然后又变回 A,在 CAS 操作时,由于最终值仍然是 A,CAS 会认为这个值没有发生变化,从而继续执行更新操作,但实际上值已经经历了变化。这在某些业务场景下可能会导致数据不一致或出现意外的结果。
● 示例场景:假设有一个栈,线程 T1 要将栈顶元素 A 出栈,同时线程 T2 先将 A 出栈,再将 A 入栈,此时栈顶元素还是 A。当 T1 执行 CAS 操作时,会认为栈顶元素没有变化,从而继续出栈操作,这可能会导致数据处理异常。
● 解决方案:可以使用带有版本号的 CAS 操作,即每次修改值时,不仅更新值本身,还更新一个版本号。Java 中提供了 AtomicStampedReference 类来解决 ABA 问题,它在进行 CAS 操作时会同时比较值和版本号。以下是一个示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 0);
int stamp = atomicStampedReference.getStamp();
// 模拟 ABA 过程
atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1);
atomicStampedReference.compareAndSet(101, 100, stamp + 1, stamp + 2);
// 尝试更新
boolean result = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 3);
System.out.println("更新结果: " + result);
}
}
在上述代码中,AtomicStampedReference 会同时检查值和版本号,只有当值和版本号都符合预期时,才会执行更新操作,从而避免了 ABA 问题。
2. 循环时间长开销大
● 问题描述:在 CAS 操作中,如果竞争非常激烈,CAS 操作可能会多次失败,从而导致线程不断地进行重试,这会消耗大量的 CPU 资源,使程序的性能下降。
● 解决方案:可以通过限制重试次数或者使用锁机制来解决。例如,在某些情况下,可以设置一个最大重试次数,当超过这个次数时,放弃 CAS 操作,采用其他方式处理;或者在竞争激烈的场景下,直接使用传统的锁机制,如 synchronized 关键字。以下是一个限制重试次数的示例:
import java.util.concurrent.atomic.AtomicInteger;
public class CASRetryLimit {
private static AtomicInteger atomicInteger = new AtomicInteger(10);
private static final int MAX_RETRIES = 5;
public static void updateValue() {
int retries = 0;
while (retries < MAX_RETRIES) {
int current = atomicInteger.get();
int next = current + 1;
if (atomicInteger.compareAndSet(current, next)) {
System.out.println("更新成功,当前值: " + next);
return;
}
retries++;
}
System.out.println("更新失败,达到最大重试次数");
}
public static void main(String[] args) {
updateValue();
}
}
在上述代码中,当 CAS 操作失败时,会进行重试,但最多重试 5 次,超过这个次数则放弃更新操作。
3. 只能保证一个共享变量的原子操作
● 问题描述:CAS 操作只能对一个共享变量进行原子操作,如果需要对多个共享变量进行原子操作,CAS 就无法直接满足需求。
● 解决方案:
○ 使用 AtomicReference:可以将多个共享变量封装在一个对象中,然后使用 AtomicReference 来对这个对象进行原子操作。以下是一个示例:
import java.util.concurrent.atomic.AtomicReference;
class MyData {
int value1;
int value2;
public MyData(int value1, int value2) {
this.value1 = value1;
this.value2 = value2;
}
}
public class MultipleVariablesCAS {
private static AtomicReference<MyData> atomicReference = new AtomicReference<>(new MyData(10, 20));
public static void updateValues() {
MyData current = atomicReference.get();
MyData next = new MyData(current.value1 + 1, current.value2 + 1);
if (atomicReference.compareAndSet(current, next)) {
System.out.println("更新成功,value1: " + next.value1 + ", value2: " + next.value2);
}
}
public static void main(String[] args) {
updateValues();
}
}
使用锁机制:对于多个共享变量的原子操作,也可以使用传统的锁机制,如 synchronized
关键字或 ReentrantLock
来保证操作的原子性。
综上所述,CAS 是一种高效的并发操作机制,但在使用时需要注意其可能产生的问题,并根据具体场景选择合适的解决方案。