原子变量-AtomicStampedReference/AtomicMarkableReference

之前讲过的AtomicInteger等CAS操作会产生ABA问题,什么是ABA?wiki官方解释https://en.wikipedia.org/wiki/ABA_problem,简单讲就是多线程环境,2次读写中一个线程修改A->B,然后又B->A,另一个线程看到的值未改变,又继续修改成自己的期望值。如果我们不关心中间状态的变化,只关心最终结果,就无所谓ABA问题。看代码:

import java.util.concurrent.atomic.AtomicReference;

public class ABATest {
    static  AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(1);
    
    public static void main(String[] args) throws InterruptedException {

       new Thread(new Runnable() {
                
                @Override
                public void run() {
                    System.out.println(Thread.currentThread()+ "-" + atomicReference.compareAndSet(1, 2));
                    System.out.println(Thread.currentThread()+ "-" + atomicReference.compareAndSet(2, 1));
                }
            }).start();
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                System.out.println(Thread.currentThread()+ "-" + atomicReference.compareAndSet(1, 2));
            }
        }).start();
        
        Thread.currentThread().sleep(3000);
        System.out.println(atomicReference.get());

    }
}

一个线程将1->2->1,另一个线程看到期望值还是1,就将其改成最终值2,如果不关心第一个线程所做的中间状态的变更,也就不用关心ABA问题。 这是一个不是"问题"的问题。

看下AtomicStampedReference是怎么解决这个问题的:

/**
通过static pair保存一个引用和计数器
*/
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;

/**
 * 通过传入的初始化引用和计数器来构造函数一个pair
 *
 * @param initialRef 初始化用用
 * @param initialStamp 初始化计数器或者叫时间戳
 */
public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

AtomicStampedReference通过一个pair来保存初始化引用和计数器,以后每次原子操作时,都需要比较引用和计数器是否都正确。

举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。这就是AtomicStampedReference的解决方案。

看下AtomicStampedReference的方法:

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    //每次操作前不但比较引用值还比较计数器,底层还是unsafe那些方法
    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);
}

当然还提供了获取计数器和引用值的方法

public V getReference() {
    return pair.reference;
}

public int getStamp() {
    return pair.stamp;
}

其他方法都差不多,看个简单使用的demo:

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceTest {

    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(0, 0);

    public static void main(String[] args) throws InterruptedException {
        
        final int stamp = atomicStampedReference.getStamp();
        final Integer reference = atomicStampedReference.getReference();

        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                    System.out.println(Thread.currentThread() + "-" + reference + "-" + stamp + "-"
                            + atomicStampedReference.compareAndSet(reference, reference + 10, stamp, stamp + 1));
            }
        });
        
        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                    Integer reference = atomicStampedReference.getReference();
                    System.out.println(Thread.currentThread() + "-" + reference + "-" + stamp + "-"
                            + atomicStampedReference.compareAndSet(reference, reference + 10, stamp, stamp + 1));
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(atomicStampedReference.getReference());
        System.out.println(atomicStampedReference.getStamp());
    }
}

AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。

还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。