AtomicStampedReference

364 阅读2分钟

AtomicStampedReference

  1. AtomicStampedReference的应用场景是什么?
  2. AtomicStampedReference如何解决 ABA 问题?

AtomicStampedReference的应用场景是什么?

@Test
public  void test2() throws InterruptedException {
    AtomicInteger value = new AtomicInteger(1);

    Thread t1 = new Thread(() -> {
        System.out.println("Thread 1: Value = " + value.get()); // 输出1
        value.compareAndSet(1, 2); 
        System.out.println("Thread 1: First Value = " + value.get()); // 输出2
        System.out.println("Do Something");
        value.compareAndSet(2, 1); 
        System.out.println("Thread 1: Second Value = " + value.get()); // 输出1
    });

    Thread t2 = new Thread(() -> {
        System.out.println("Thread 2: Value = " + value.get()); // 输出1
        value.compareAndSet(1, 3); 
        System.out.println("Thread 2: Value = " + value.get()); // 输出3
    });

    t1.start();
    Thread.sleep(100);
    t2.start();
}

在 t2 中无法辨别 value 是走了 t1 线程还是原数据,因此这就产生了 ABA 问题。而采用 AtomicStampedReference 就可以很好的解决该问题

@Test
public void test() throws InterruptedException {
    AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0);

    new Thread(() -> {
        System.out.println("Thread 1: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
        atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); 
        System.out.println("Thread 1 First: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); // First: Value = 2, Stamp = 1
        int  newStamp = atomicStampedRef.getStamp();
        atomicStampedRef.compareAndSet(2, 1, newStamp, newStamp + 1); 
        System.out.println("Thread 1 Second: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp());// Second: Value = 1, Stamp = 2


    }).start();
    Thread.sleep(100);
    new Thread(() -> {
        System.out.println("Thread 2: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
        atomicStampedRef.compareAndSet(1, 3, stamp, stamp + 1); 
        System.out.println("Thread 2 Modify: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); // Modify: Value = 1, Stamp = 2
    }).start();
}

AtomicStampedReference如何解决 ABA 问题?

CAS有ABA问题的且ABA问题是针对引用型对象的,而 AtomicStampedReference 就是为了解决这一问题。 MySQL 的乐观锁通过加版本号来解决 ABA 问题,因此在 Java 中我们也可以借鉴 MySQL 的思想解决 CAS 的 ABA 问题

public class AtomicStampedReference<V> {
    // AtomicStampedReference 内部类
    private static class Pair<T> {
        final T reference;   // 存放引用对象
        final int stamp;  // 存放时间戳
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair; // 通过 volatile 修饰在多线程情况下可以发现 pair 的变化

下面的代码展示了具体如何解决 CAS 中的 ABA 问题

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))); // 新创建对象,不是修改对象属性
}

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

只有在 旧引用相同并且旧版本相同的情况下,才能进行修改。如果新版本的引用和时间戳一样则不需要进行修改,否则新创建一个 Pair 对象

在没看源码之前,我以为会通过 CAS 修改 Pair 的两个属性去达到最终一致性,但是源码是新创建一个对象。

我猜想可能是因为修改两个属性的话,CAS 不好操作,CAS 修改单个变量比较好操作,而同时对两个变量不是很好操作,如果想要保证同时对两个变量进行操作需要加锁,反而让问题变得更加复杂,不如新创建一个对象。而新创建一个对象通过 CAS 判断就比较简单,就相当于只对单个变量进行 CAS 操作