【JDK源码】并发原子类AtomicStampedReference

简介

AtomicStampedReference是java并发包下提供的一个原子类,它能解决其它原子类无法解决的ABA问题,比如AtomicInteger存在ABA问题

ABA

ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。

比如,两个线程按下面的顺序执行:

(1)线程1读取内存位置X的值为A;

(2)线程1阻塞了;

(3)线程2读取内存位置X的值为A;

(4)线程2修改内存位置X的值为B;

(5)线程2修改又内存位置X的值为A;

(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;

在这里插入图片描述

可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。

ABA问题通常发生在无锁结构中,用代码来表示上面的过程大概就是这样:

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(1);
    new Thread(new Runnable() {
        @Override
        public void run() {
            int value = atomicInteger.get();
            System.out.println("线程1读到value="+value);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (atomicInteger.compareAndSet(value,3)) {
                System.out.println("线程1修改value从"+value+"到3");
            }
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            int value = atomicInteger.get();
            System.out.println("线程2读到value="+value);
            if (atomicInteger.compareAndSet(value,2)) {
                System.out.println("线程2修改value从"+value+"到2");

                value = atomicInteger.get();
                System.out.println("线程2读到value="+value);
                if (atomicInteger.compareAndSet(value,1)){
                    System.out.println("线程2修改value从"+value+"到1");
                }
            }
        }
    }).start();
}

输出

线程1读到value=1
线程2读到value=1
线程2修改value从12
线程2读到value=2
线程2修改value从21
线程1修改value从13

内部类

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);
    }
}

将元素值和版本号绑定在一起,存储在Pair的reference和stamp(邮票、戳的意思)中。

构造方法

public AtomicStampedReference(V initialRef, int initialStamp) {
    // 也就是初始化的时候必须传入元素值和版本号
    pair = Pair.of(initialRef, initialStamp);
}

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) ||
         // 构造新的Pair对象并CAS更新
         casPair(current, Pair.of(newReference, newStamp)));
}
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
    objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

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

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

  • AtomicStampedReference采用版本号来解决ABA

案例

使用AtomicStampedReference解决那个AtomicInteger带来的ABA问题

public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1,1);
    int[] stampHolder = new int[1];

    new Thread(new Runnable() {
        @Override
        public void run() {
            int  value = atomicInteger.get(stampHolder);
            // 一定要先保存原来的值
            int stamp = stampHolder[0];
            System.out.println("线程1读到value="+value+"、stamp="+stampHolder[0]);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1读到value="+value+"、stamp="+stampHolder[0]);
            // 这里变成stamp是为了传入之前的值,如果是stampHolder[0]则是最新的值也就没有意义了
            if (atomicInteger.compareAndSet(value,3,stamp,stampHolder[0]+1)) {
                System.out.println("线程1修改value从"+value+"到3、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));
            }else{
                System.out.println("线程1修改失败");
            }
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            int  value = atomicInteger.get(stampHolder);
            System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
            if (atomicInteger.compareAndSet(value,2,stampHolder[0],stampHolder[0]+1)) {
                System.out.println("线程2修改value从"+value+"到2、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));

                value = atomicInteger.get(stampHolder);
                System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
                if (atomicInteger.compareAndSet(value,1,stampHolder[0],stampHolder[0]+1)){
                    System.out.println("线程2修改value从"+value+"到1、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));
                    value = atomicInteger.get(stampHolder);
                    System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
                }
            }
        }
    }).start();
}

可以看到线程1最后更新1到3时失败了,因为这时版本号也变了,成功解决了ABA的问题。

线程2读到value=1、stamp=1
线程1读到value=1、stamp=1
线程2修改value从12、stamp从12
线程2读到value=2、stamp=2
线程2修改value从21、stamp从23
线程2读到value=1、stamp=3
线程1读到value=1、stamp=3
线程1修改失败

总结

(1)在多线程环境下使用无锁结构要注意ABA问题;

(2)ABA的解决一般使用版本号来控制,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值;

(3)AtomicStampedReference内部使用Pair来存储元素值及其版本号;

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>