解决 ABA 问题
CAS (Compare and set)很好用,但是也有问题:1、CAS 本质上是自旋锁,在锁竞争比较激烈的情况下或者单核 CPU 的情况下,性能并不高。这个问题不算严重,最多就是使用不当性能差点;2、存在 ABA 问题。这个问题比较严重,可能导致数据不一致的问题。
CAS 需要在操作的时候检查数据有没有变化,如果没有发生变化就更新,但是如果一个数据本来是 A,第一个线程看到的也是 A,此时又另外一个线程把数据改成 B,又改成 A,此时第一个线程看到的还是 A,但其实数据已经发生了变化。
ABA 是会产生问题的。设想一种情况,用来本来有 50 积分,用户提交了充值 100 积分的订单,如果一个线程要给用户充 100 积分,此时另外一个线程抢先一步给这个用户充值了 100 积分,然后用户把这 100 积分消费掉,此时第一个线程看到的仍然是 50 积分,以为数据没有变化,于是再给 用户充值 100 积分。虽然这种情况发生的概率很小,但是在高并发的情况下是有可能发生的。
产生 ABA 问题的根源在于第一个看到数据的线程没有感知到数据的变化,解决这个问题的做法是添加版本号,基于数据库的乐观锁就是这个做的,在 java 标准库里也有类似的实现:AtomicStampedReference
。
AtomicStampedReference
内部加了一个时间戳作为版本号。
基本方法如下:
//构造方法, 传入引用和戳
public AtomicStampedReference(V initialRef, int initialStamp)
//返回引用
public V getReference()
//返回版本戳
public int getStamp()
//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
//如果当前引用 等于 预期引用, 将更新新的版本戳到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//设置当前引用的新引用和版本戳
public void set(V newReference, int newStamp)
看下面的例子:
public class ABATest {
public static void main(String[] args) {
testStamp();
}
private static void testStamp() {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
new Thread(()->{
int[] stampHolder = new int[1];
int value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("thread 1 read value: " + value + ", stamp: " + stamp);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
System.out.println("thread 1 update from " + value + " to 3");
} else {
System.out.println("thread 1 update fail!");
}
}).start();
new Thread(()->{
int[] stampHolder = new int[1];
int value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
System.out.println("thread 2 update from " + value + " to 2");
// do sth
value = atomicStampedReference.get(stampHolder);
stamp = stampHolder[0];
System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
System.out.println("thread 2 update from " + value + " to 1");
}
}
}).start();
}
}