Published on

解决 ABA 问题

Authors

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();
    }
}

Class AtomicStampedReference<V>

Guide to AtomicStampedReference in Java

深入理解 CAS 与 ABA 问题