高并发编程之原子类AtomicStampedReference—源码剖析

AtomicStampedReference是Java提出的一个原子类,它可以解决ABA问题。与这个类有同样作用的类是AtomicMarkableReference,他们都可以解决CAS的ABA问题。

一、什么是ABA问题?ABA的危害?

ABA问题发生在多线程环境下,说的是当线程A对一个变量使用CAS修改的时候,他两次读到内存地址V中的值一样,然后改线程就简单的认为变量没有发生过修改。然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。

如上图所示,在线程1两次读取到变量的值都是A,但是在这中间线程2对这个变量做了修改,但是对于线程1来说他却感受不到。

ABA问题在有些场景中不会产生问题,但是在有些场景中是十分要命的,比如下面这个例子:

假设有一个遵循CAS原理的提款机,小明有100元存款,要用这个提款机来取50元。由于提款机硬件出了点小问题,小明的提款操作被同时提交两次,开启了两个线程(线程1,2),两个线程都是获取当前值100元,要更新成50元。正常情况下,应该一个线程更新成功,另一个线程再更新就会失败,小明的存款只被扣一次。

  • 线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小明的妈妈刚好给小明汇款50元(线程3)。

  • 线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。

  • 线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以“成功”把变量值100更新成了50。这是不合理的。

因此当遇到这样的场景时我们就需要预防CAS带来的ABA问题,Java中提供了AtomicStampedReferenceAtommicMarkableReference这两个类来解决ABA问题。他们两个实现的手段大致相同,只是前者是通过使用一个int类型的版本号来控制修改的,只有当预期值和内存地址V中的值相等并且版本号一致时才允许修改,而后者是通过一个boolean类型的变量来说明变量是否被修改过,下面我就以AtomicStampedReference深入源码分析他的机制。

二、AtomicStampedReference源码分析

1、内部类

Pair是AtomicStampedReference的内部类,它主要的作用就是把元素和版本号绑定起来。

2、属性
private volatile Pair<V> pair;
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

在AtomicStampedReference中有一个volatile修饰的成员变量Pair类型的pair。pair中保存了reference(元素引用)和版本号(stamp)并且使用Unsafe获取到其偏移量,存储到pairOffset变量中,这个变量在调用Unsafe类中的方法是有用。

3、构造方法

AtomicStampedReference类只有一个构造方法,第一个参数就是元素的引用,第二个是初始版本号。使用方法如下:

public class AtomicStampedReferenceTest {

    public static void main(String[] args) {
        BigDecimal decimal = new BigDecimal(666.666);
        AtomicStampedReference<BigDecimal> ref = new AtomicStampedReference<>(decimal, 1);
        System.out.println(ref.compareAndSet(decimal, BigDecimal.valueOf(888.88), 1, ref.getStamp() + 1));
        System.out.println(ref.getReference());
    }

}
4、compareAndSet()方法

  • expectedReference:表示预期引用
  • newReference:新的引用
  • expectedStamp:预期版本号
  • newStamp:更新后的版本号

整个方法的逻辑是:先获取到当前的pair对象(也就是当前的旧引用值和版本号),之后比较如果当前pair中的引用值和预期引用值相等并且当前版本和预期版本相等的时候,如果新的引用和旧的引用不相等并且者新的版本号和旧的版本号不相等时才会调用casPair()使用CAS的方式修改pair中的值,反之有一处不符合就不会发生替换。下面是casPair()的实现细节:

可以看到,底层最终还是调用了Unsafe类的另一个CAS方法compareAndSwapObject

总结一下compareAndSet()方法的逻辑就是:

  • (1)如果元素值和版本号都没有变化,并且和新的也相同,返回true;

  • (2)如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。

可以看到,java中的实现手段:

  • 首先,使用版本号控制

  • 其次,不重复使用节点(Pair)的引用,每次都新建一个新的Pair来作为CAS比较的对象,而不是复用旧的

  • 最后,外部传入元素值及版本号,而不是节点(Pair)的引用。

留言区

还能输入500个字符