CAS产生的ABA问题

Posted by Kaka Blog on March 15, 2019

CAS的实现

JAVA中的cas主要使用的是Unsafe方法,Unsafe的CAS操作主要是基于硬件平台的汇编指令,目前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。

Unsafe提供了三个方法用于CAS操作,分别是

public final native boolean compareAndSwapObject(Object value, long valueOffset, Object expect, Object update);

public final native boolean compareAndSwapInt(Object value, long valueOffset, int expect, int update);

public final native boolean compareAndSwapLong(Object value, long valueOffset, long expect, long update);
  • value 表示 需要操作的对象
  • valueOffset 表示 对象(value)的地址的偏移量(通过Unsafe.objectFieldOffset(Field valueField) 获取)
  • expect 表示更新时value的期待值
  • update 表示将要更新的值

具体过程为每次在执行CAS操作时,线程会根据valueOffset去内存中获取当前值去跟expect的值做对比如果一致则修改并返回true,如果不一致说明有别的线程也在修改此对象的值,则返回false。

ABA问题

线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。

例子:

public static AtomicInteger a = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
    Thread main = new Thread(() -> {
        System.out.println("操作线程" + Thread.currentThread() +",初始值 = " + a);  //定义变量 a = 1
        try {
            Thread.sleep(1000);  //等待1秒 ,以便让干扰线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }System.out.println("操作线程" + Thread.currentThread() +",CAS前初始值 = " + a);
        boolean isCASSuccess = a.compareAndSet(1,2); // CAS操作
        System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
    },"主操作线程");

    Thread other = new Thread(() -> {
        a.incrementAndGet(); // a 加 1, a + 1 = 1 + 1 = 2
        System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ a);
        a.decrementAndGet(); // a 减 1, a - 1 = 2 - 1 = 1
        System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ a);
    },"干扰线程");

    main.start();
    other.start();
    main.join();
    System.out.println("a = " + a);
}

输出:

操作线程Thread[主操作线程,5,main],初始值 = 2
操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
操作线程Thread[主操作线程,5,main],CAS前初始值 = 1
操作线程Thread[主操作线程,5,main],CAS操作结果: true
a = 2

解决ABA

解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操作时都对比此版本号。AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数”stamp”的pair对象来解决ABA问题。

private static AtomicStampedReference atomicStampedRef =
            new AtomicStampedReference(1, 0);
public static void main(String[] args) throws InterruptedException {
    Thread main = new Thread(() -> {
        System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
        int stamp = atomicStampedRef.getStamp(); //获取当前标识别
        try {
            Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
        System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
    },"主操作线程");

    Thread other = new Thread(() -> {
        atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
        atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
    },"干扰线程");

    main.start();
    other.start();
    main.join();
    System.out.println("操作线程" + Thread.currentThread() +",结果 a = " + atomicStampedRef.getReference());
}

输出:

操作线程Thread[主操作线程,5,main],初始值 a = 2
操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
操作线程Thread[主操作线程,5,main],CAS操作结果: false
操作线程Thread[main,5,main],结果 a = 1

参考