目录
引言
在并发编程中,CAS(Compare-And-Swap)是一种非常重要的无锁机制,用于实现线程安全。然而,CAS 并非完美无缺,它存在一个经典的问题——ABA 问题。本文将深入探讨 CAS 的工作原理、ABA 问题的成因及其解决方案。
1. 什么是 CAS?
CAS(Compare-And-Swap)是一种原子操作,用于在多线程环境下实现无锁的线程安全。它的核心思想是:
-
比较:检查某个内存位置的值是否与预期值相等。
-
交换:如果相等,则将该内存位置的值更新为新值;否则,不做任何操作。
CAS 操作是原子的,即在执行过程中不会被其他线程打断。
CAS 的伪代码
boolean compareAndSwap(V, A, B) {
if (V == A) {
V = B;
return true;
}
return false;
}
2. CAS 的工作原理
CAS 操作通常涉及三个参数:
-
内存地址(V):需要更新的变量。
-
预期值(A):认为变量当前应该具有的值。
-
新值(B):希望将变量更新为的值。
CAS 的操作步骤如下:
-
读取内存地址 V 的当前值。
-
比较当前值是否等于预期值 A。
-
如果相等,则将内存地址 V 的值更新为新值 B。
-
如果不相等,则操作失败(通常需要重试)。
-
-
返回操作是否成功。
用代码理解下什么是CAS:
package com.kuang;
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS : 比较并交换 compareAndSet
*
* 参数:期望值,更新值
* public final boolean compareAndSet(int expect, int update) {
* return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
* }
* @author 狂神说Java 24736743@qq.com
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// main do somethings...
// 期望的是5,后面改为 2020 , 所以结果为 true,2020
System.out.println(atomicInteger.compareAndSet(5,
2020)+"=>"+atomicInteger.get());
// 期望的是5,后面改为 1024 , 所以结果为 false,2020
System.out.println(atomicInteger.compareAndSet(5,
1024)+"=>"+atomicInteger.get());
}
}
一句话:真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!
3. CAS 的实现
在 Java 中,CAS 操作是通过 Unsafe
类或 java.util.concurrent.atomic
包中的原子类(如 AtomicInteger
)实现的。
3.1 AtomicInteger
示例
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger value = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = value.get(); // 获取当前值
newValue = oldValue + 1; // 计算新值
} while (!value.compareAndSet(oldValue, newValue)); // CAS 更新
}
public int getValue() {
return value.get();
}
}
CAS 的缺点
1、循环时间长开销很大。
可以看到源码中存在 一个 do...while 操作,如果CAS失败就会一直进行尝试。
2、只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。
3、引出来 ABA 问题???
提出问题 => 原子类 AtomicInteger 的ABA问题谈谈?原子更新引用知道吗?
CAS => UnSafe => CAS 底层思想 => ABA => 原子引用更新 => 如何规避ABA问题
4. 什么是 ABA 问题?
ABA 问题是 CAS 操作中的一个经典问题。它描述的是以下场景:
-
线程 1 读取变量 V 的值为 A。
-
线程 1 被挂起。
-
线程 2 将变量 V 的值从 A 修改为 B。
-
线程 2 再次将变量 V 的值从 B 修改回 A。
-
线程 1 恢复执行,发现变量 V 的值仍然是 A,认为没有变化,于是 CAS 操作成功。
尽管 CAS 操作成功,但实际上变量 V 的值已经经历了从 A 到 B 再到 A 的变化。这种变化可能会导致程序逻辑错误。
5. ABA 问题的危害
ABA 问题在某些场景下可能会导致严重的后果。例如:
-
链表操作:在无锁链表中,如果一个节点的值从 A 变为 B 又变回 A,CAS 操作可能会错误地认为链表没有变化,从而导致数据丢失或链表损坏。
-
资源管理:在资源池中,如果一个资源的状态从“空闲”变为“使用中”又变回“空闲”,CAS 操作可能会错误地认为资源一直处于“空闲”状态。
6. 解决 ABA 问题的方法(原子引用)
为了解决 ABA 问题,可以引入版本号或时间戳机制。每次更新变量时,不仅更新值,还更新版本号或时间戳。这样,即使值相同,版本号或时间戳不同,CAS 操作也会失败。(类似乐观锁)
6.1 AtomicStampedReference
Java 提供了 AtomicStampedReference
类来解决 ABA 问题。它通过维护一个版本号(stamp)来避免 ABA 问题。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);
public static void main(String[] args) {
int stamp = value.getStamp(); // 获取当前版本号
Integer oldValue = value.getReference(); // 获取当前值
// 尝试更新值和版本号
boolean success = value.compareAndSet(oldValue, oldValue + 1, stamp, stamp + 1);
if (success) {
System.out.println("更新成功!");
} else {
System.out.println("更新失败!");
}
}
}
6.2 AtomicMarkableReference
AtomicMarkableReference
是另一种解决 ABA 问题的方式,它通过一个布尔标记来区分不同的状态。
import java.util.concurrent.atomic.AtomicMarkableReference;
public class ABASolution {
private static AtomicMarkableReference<Integer> value = new AtomicMarkableReference<>(0, false);
public static void main(String[] args) {
boolean[] markHolder = new boolean[1];
Integer oldValue = value.get(markHolder); // 获取当前值和标记
// 尝试更新值和标记
boolean success = value.compareAndSet(oldValue, oldValue + 1, markHolder[0], !markHolder[0]);
if (success) {
System.out.println("更新成功!");
} else {
System.out.println("更新失败!");
}
}
}
7. CAS 的优缺点
优点:
-
无锁:CAS 不需要加锁,避免了锁带来的性能开销和死锁风险。
-
高性能:在低竞争环境下,CAS 的性能优于锁机制。
-
可扩展性:CAS 适用于高并发场景,能够有效提高程序的并发性能。
缺点:
-
ABA 问题:CAS 只能检查值是否相等,无法感知值的变化过程。
-
自旋开销:在高竞争环境下,CAS 可能需要多次重试,导致 CPU 资源浪费。
-
只能保证一个变量的原子性:CAS 只能对一个变量进行原子操作,无法支持多个变量的复合操作。
8. 总结
CAS 是一种高效的无锁线程安全机制,广泛应用于并发编程中。然而,CAS 存在 ABA 问题,可能导致程序逻辑错误。通过引入版本号或时间戳机制(如 AtomicStampedReference
或 AtomicMarkableReference
),可以有效解决 ABA 问题。