目录
什么是 CAS
CAS(Compare-And-Swap)是一种用于在多线程编程中实现原子操作的技术,是硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
CAS 的工作原理
三个关键参数
-
内存位置(V):需要检查和更新的变量的内存地址。
-
预期原值(A):线程预期的变量当前值。
-
新值(B):如果变量的当前值与预期原值相匹配,那么将变量更新为这个新值。
CAS 操作基本步骤
-
比较:首先,线程会检查内存位置的值是否与预期原值相等。
-
交换:如果值相等,线程会将内存位置的值更新为新值。
-
返回:操作完成后,CAS会返回一个布尔值,表示操作是否成功。
如果内存位置的值在比较和交换之间被其他线程更改,那么CAS操作会失败,因为内存位置的值与预期原值不再匹配。
CAS 的核心 UnSafe
UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问, UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。
Unsafe类中的所有方法都是Native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
CAS 的使用
下面是一个简单案例:
public class CASDemo {
public static void main(String[] args) {
// 创建一个 AtomicInteger 对象,并初始化为 5
AtomicInteger atomicInteger = new AtomicInteger(5);
// 使用 compareAndSet 方法尝试将 atomicInteger 的值从 5 改为 2020
// 如果当前值是 5,则修改成功,返回 true,并且 atomicInteger 的值变为 2020
// 否则,修改失败,返回 false,atomicInteger 的值保持不变
// 期望的结果是 true,因为当前值确实是 5
System.out.println(atomicInteger.compareAndSet(5, 2020) + "=>" + atomicInteger.get());
// 再次使用 compareAndSet 方法尝试将 atomicInteger 的值从 5 改为 1024
// 由于上一步操作已经将 atomicInteger 的值改为 2020,这次比较会失败
// 因此,返回 false,atomicInteger 的值保持为 2020
// 期望的结果是 false,因为当前值不再是 5
System.out.println(atomicInteger.compareAndSet(5, 1024) + "=>" + atomicInteger.get());
}
}
-
第一次
compareAndSet
调用成功,因为atomicInteger
的值确实是5,所以值被更新为2020。 -
第二次
compareAndSet
调用失败,因为atomicInteger
的值已经是2020,与预期值5不匹配,所以值保持不变,仍然是2020。
CAS 优缺点
优点
-
无锁并发:CAS操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。
-
原子性:CAS操作是原子的,保证了线程安全。
缺点
-
ABA问题:CAS操作中,如果一个变量值从A变成B,又变回A,CAS无法检测到这种变化,可能导致错误。
-
自旋开销:CAS操作通常通过自旋实现,可能导致CPU资源浪费,尤其在高并发情况下。
-
单变量限制:CAS操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。
下面我们来详细讲一讲 ABA 问题。
什么是 ABA 问题
ABA 问题是指在并发编程中,当一个变量的值从A变为B,再从B变回A时,如果使用 CAS 操作,该操作可能会错误地认为变量的值没有改变,从而更新变量为另一个值,这可能导致数据不一致的问题。
ABA 问题示例:
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo {
private static AtomicInteger value = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
value.set(1); // 线程1将值设置为1
value.set(0); // 线程1将值改回0
});
Thread thread2 = new Thread(() -> {
int originalValue = value.get(); // 读取值为0
while (originalValue == 0) {
if (value.compareAndSet(originalValue, 2)) { // 尝试将0改为2
System.out.println("Value changed to 2");
break;
}
originalValue = value.get(); // 重新读取值
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
本示例中,thread1
将value
从0改为1,然后又改回0。thread2
尝试将value
从0改为2。由于thread1
的操作,thread2
可能会错误地认为value
仍然是0,并成功地将其改为2,即使value
实际上已经经历了变化。
ABA 问题的解决
解决ABA问题的常见方法是使用带有版本号的原子引用类,如AtomicStampedReference
。这个类通过维护一个“版本号”或“时间戳”来确保操作的原子性和正确性,类似乐观锁。
使用AtomicStampedReference
解决ABA问题的代码示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolutionDemo {
private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
int[] stamp = new int[1];
value.getReference(stamp); // 获取当前值和版本号
value.set(1, stamp[0] + 1); // 线程1将值设置为1,并增加版本号
value.set(0, stamp[0] + 1); // 线程1将值改回0,并增加版本号
});
Thread thread2 = new Thread(() -> {
int[] stamp = new int[1];
while (value.getReference(stamp) == 0) {
if (value.compareAndSet(0, 2, stamp[0], stamp[0] + 1)) { // 尝试将0改为2,并增加版本号
System.out.println("Value changed to 2");
break;
}
stamp[0] = value.getStamp(); // 重新获取版本号
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
在这个解决方案中,AtomicStampedReference
维护了一个值和一个版本号。每次更新值时,版本号都会增加,这样即使值被改回原来的值,版本号也已经改变,从而避免了ABA问题。compareAndSet
方法现在需要检查值和版本号是否都匹配,只有两者都匹配时才会更新值和版本号。