CAS之ABA问题的解决方法

771 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

AtomicReference

java.util.concurrent.atomic包下的AtomicInteger类可以对整数进行包装,使得类似于i++的操作可以变成原子操作,那么对于一般的对象类型要怎么实现原子操作呢,从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性。

public class AtomicReferenceDemo {
    static AtomicReference<Student> stu = new AtomicReference<>();

    public static void main(String[] args) {
        Student lemon = new Student("lemon", 17);
        stu.set(lemon);
        Student blue = new Student("blue", 19);
        boolean res = stu.compareAndSet(lemon, blue);
        System.out.println(res + "\tstu=" + stu.get());
    }
}

运行结果:

true stu=Student{name='blue', age=19}

但是和原来使用AtomicInteger一样,CAS的时候仍旧会产生ABA问题

产生ABA问题的代码

public class ABADemo {
    public static void main(String[] args) {
        //ABA问题的代码
        AtomicReference<Integer> atomicReference = new AtomicReference<>();
        atomicReference.set(100); //原来的值是100
        new Thread(() -> {
            boolean res1 = atomicReference.compareAndSet(100, 101);
            boolean res2 = atomicReference.compareAndSet(101, 100);
            System.out.println("res1=" + res1);
            System.out.println("res2=" + res2);
        }).start();
        new Thread(() -> {
            boolean res3 = atomicReference.compareAndSet(100, 200);
            System.out.println("res3=" + res3);
        }).start();

    }

}

运行结果:res1,res2,res3都是true,这说明虽然线程t1修改了atomicReference中的值,但是由于修改后值与原来的一样,所以线程t2在判断的时候认为该值没有被修改过CAS操作成功。

AtomicStampedReference解决ABA问题

为了解决ABA问题,引入了AtomicStampedReference,AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(版本号)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。

AtomicStampedReference的构造器

在初始化的时候传入两个参数,初始化的引用值和版本号

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

AtomicStampedReference的compareAndSet方法

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
  • expectedReference : 期望值
  • newReference : 想要更新成的新的值
  • expectedStamp : 期望的版本号
  • newStamp : CAS操作成功要更新成的版本号 然后每次操作的时候都会先比较版本号,版本号一致才能操作成功,每次操作成功后都将版本号增加+1(版本号只加不减)

测试代码

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolve {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
        int stamp = atomicStampedReference.getStamp();
        new Thread(() -> {
            System.out.println("t1线程拿到的初始版本号:" + stamp);
            System.out.println("t1线程拿到的初始值:" + atomicStampedReference.getReference());
            //
            boolean res1 = atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp() + 1);
            System.out.println("修改结果:" + res1);
            System.out.println("t1线程修改之后的版本号:" + atomicStampedReference.getStamp());
            System.out.println("t1线程修改之后的值:" + atomicStampedReference.getReference());
            boolean res2 = atomicStampedReference.compareAndSet(101, 100, 2, atomicStampedReference.getStamp() + 1);
            System.out.println("修改结果:" + res2);

        }).start();
        new Thread(() -> {
            System.out.println("t2线程拿到的初始版本号:" + stamp);
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean b1 = atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp() + 1);
            System.out.println("t2线程期望的版本号为" + stamp + ",t2线程当前查看主内存中atomicStampedReference真实的版本号为:" + atomicStampedReference.getStamp());
            System.out.println("t2线程CAS操作的结果为:" + b1);
        }).start();
    }
}

该代码模拟了线程1将atomicStampedReference值修改后又改回成原来的值的过程,观察版本号的变化以及最后线程2的CAS操作成功与否,运行结果如下:

t1线程拿到的初始版本号:1
t1线程拿到的初始值:100
修改结果:true
t1线程修改之后的版本号:2
t1线程修改之后的值:101
修改结果:true
t2线程拿到的初始版本号:1
t2线程期望的版本号为1,t2线程当前查看主内存中atomicStampedReference真实的版本号为:3
t2线程CAS操作的结果为:false

总结

1.AtomicReference使得对象类型可以想整数类型一样保证包装,实现原子操作

2.AtomicStampedReference在原来AtomicReference基础上加入了stamp(版本号)这一属性,每次操作成功必定增加版本号,使得在CAS的时候不会出现ABA问题(因为数据被修改过版本号肯定不一样)。