Java CAS操作的ABA问题与AtomicStampedReference的解决
Java中的CAS(Compare-And-Swap)是一种无锁算法,用于实现多线程环境下的原子操作。它通过比较内存中的值与预期值,如果相等则更新为新值,否则不做任何操作。这种机制在java.util.concurrent.atomic包下的原子类中被广泛使用,如AtomicInteger、AtomicLong等。
然而,CAS操作存在一个经典问题——ABA问题。本文将深入探讨ABA问题的成因,并介绍如何使用AtomicStampedReference来解决它。
1. 理解ABA问题
ABA问题是指,当一个线程读取一个变量值A后,另一个线程将其修改为B,再修改回A。此时,第一个线程再次读取该变量,发现值仍然是A,并认为该变量未被其他线程修改过。但实际上,变量已经被修改过两次。
1.1 ABA问题的简单示例
假设有一个共享变量value,初始值为A。线程T1和线程T2同时操作这个变量。
- 线程
T1读取value,得到值A。 - 线程
T2将value的值从A修改为B。 - 线程
T2又将value的值从B修改回A。 - 线程
T1再次尝试使用CAS操作将value从A修改为C。
在这个场景中,T1的CAS操作会成功,因为它读取的值仍然是A。但是,T1并不知道value在它读取之后被修改过两次。这就是ABA问题。
1.2 ABA问题的危害
在复杂的并发场景中,ABA问题可能导致严重的逻辑错误。例如,在一个链表中,一个节点被删除后又重新插入,对于只关心节点值是否变化的线程来说,它可能无法察觉到这个节点的变化,从而导致链表结构错误或数据不一致。
2. 使用AtomicStampedReference解决ABA问题
为了解决ABA问题,Java提供了AtomicStampedReference类。它不仅记录了变量的值,还记录了一个“版本号”或“时间戳”。每次修改变量时,版本号也会随之增加。这样,CAS操作不仅会比较变量的值,还会比较版本号,从而避免了ABA问题。
2.1 AtomicStampedReference的工作原理
AtomicStampedReference内部维护了一个对象引用和一个整数戳记(stamp)。当调用compareAndSet方法时,它会同时比较引用和戳记。只有当引用和戳记都匹配时,才会执行更新操作。
2.2 代码示例:对比CAS和AtomicStampedReference
下面通过两个示例来对比CAS和AtomicStampedReference在处理ABA问题上的差异。
示例1:使用CAS模拟ABA问题
在这个示例中,我们将使用AtomicInteger来模拟CAS操作,并展示ABA问题。
import java.util.concurrent.atomic.AtomicInteger;
public class CASABAProblem {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
// 线程T1:读取初始值100
Thread t1 = new Thread(() -> {
int expectedValue = atomicInteger.get();
System.out.println("线程T1读取到初始值: " + expectedValue);
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试将值从100更新为101
boolean success = atomicInteger.compareAndSet(expectedValue, 101);
System.out.println("线程T1 CAS操作结果: " + success + ", 当前值: " + atomicInteger.get());
});
// 线程T2:修改值
Thread t2 = new Thread(() -> {
// 将值从100修改为101
atomicInteger.compareAndSet(100, 101);
System.out.println("线程T2将值从100修改为101");
// 再将值从101修改回100
atomicInteger.compareAndSet(101, 100);
System.out.println("线程T2将值从101修改回100");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
运行结果分析:
- 线程
T1读取到初始值100。 - 线程
T2先将值修改为101,再修改回100。 - 线程
T1在休眠后,尝试将值从100更新为101。由于此时值确实是100,CAS操作成功,但线程T1并不知道值已经被修改过两次。
示例2:使用AtomicStampedReference解决ABA问题
现在,我们使用AtomicStampedReference来解决这个问题。
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceSolution {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
// 获取初始值和初始版本号
int[] stampHolder = new int[1];
int expectedReference = atomicStampedRef.get(stampHolder);
int expectedStamp = stampHolder[0];
System.out.println("线程T1读取到初始值: " + expectedReference + ", 初始版本号: " + expectedStamp);
// 线程T1:尝试更新值
Thread t1 = new Thread(() -> {
int[] stampHolder = new int[1];
int expectedReference = atomicStampedRef.get(stampHolder);
int expectedStamp = stampHolder[0];
System.out.println("线程T1读取到初始值: " + expectedReference + ", 初始版本号: " + expectedStamp);
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试将值从100更新为101,并增加版本号
boolean success = atomicStampedRef.compareAndSet(expectedReference, 101, expectedStamp, expectedStamp + 1);
System.out.println("线程T1 CAS操作结果: " + success + ", 当前值: " + atomicStampedRef.getReference() + ", 当前版本号: " + atomicStampedRef.getStamp());
});
// 线程T2:修改值
Thread t2 = new Thread(() -> {
int[] stampHolder = new int[1];
int expectedReference = atomicStampedRef.get(stampHolder);
int expectedStamp = stampHolder[0];
// 将值从100修改为101,并增加版本号
atomicStampedRef.compareAndSet(expectedReference, 101, expectedStamp, expectedStamp + 1);
System.out.println("线程T2将值从100修改为101,版本号变为: " + (expectedStamp + 1));
// 再将值从101修改回100,并增加版本号
expectedReference = atomicStampedRef.get(stampHolder);
expectedStamp = stampHolder[0];
atomicStampedRef.compareAndSet(expectedReference, 100, expectedStamp, expectedStamp + 1);
System.out.println("线程T2将值从101修改回100,版本号变为: " + (expectedStamp + 1));
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
运行结果分析:
- 线程
T1读取到初始值100和初始版本号0。 - 线程
T2先将值从100修改为101,版本号变为1,再将值从101修改回100,版本号变为2。 - 线程
T1在休眠后,尝试将值从100更新为101。此时,虽然值是100,但版本号是2,与线程T1读取时的版本号0不匹配,因此CAS操作失败。
3. 核心结论
CAS操作在多线程环境下可能遇到ABA问题,即变量被修改后又恢复原值,导致CAS操作误判。AtomicStampedReference通过引入版本号(或时间戳)机制,在CAS操作时同时比较值和版本号,从而有效解决了ABA问题。- 在需要确保变量在读取和写入期间未被修改的场景中,应优先考虑使用
AtomicStampedReference而不是简单的CAS操作。

暂无评论,快来抢沙发吧!