聊聊高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference源码来看如何解决CAS的ABA问题

在聊聊高并发(十一)实现几种自旋锁(五)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用AtomicStampedReference原子变量而不是使用AtomicReference是因为这个实现中等待队列的同一个节点具备不同的状态,而同一个节点会多次进出工作队列,这就有可能出现出现ABA问题。


熟悉并发编程的同学应该知道CAS操作存在ABA问题。我们先看下CAS操作。

CAS(Compare and Swap) 比较并交换操作是一个三元操作: 目标地址的值T(arget),期望值E(xpected),实际值R(eal),

1. 只有当目标值T == 期望值E时,才会把目标值T设置为实际值R,否则不改变目标值

2. 不管目标值是否改变,都返回之前的目标值T


类似如下的逻辑:

package com.zc.lock;

public class CAS {
	private int value;
	
	public synchronized int get(){
		return value;
	}
	
	public synchronized int compareAndSwap(int expected, int real){
		int oldValue = value;
		if(value == expected){
			value = real;
		}
		return oldValue;
	}
	
	public synchronized boolean compareAndSet(int expected, int real){
		return (expected == compareAndSwap(expected, real));
	}
}

CAS只比较期望值和目标值是否相当,相当就设置新值。那么ABA问题就来了:

1. 由于CAS只是值比较,比如目标是A, 期望值也是A, 那么CAS操作会成功。但是这时候目标A可能不是原来的那个A了,它可能是A变成了B,再变成了A。所以叫ABA问题,很形象。ABA问题可能会使程序出错,比如限时有界队列锁中的节点有几个状态,虽然引用值是A,但是可能对象的状态已经变了,这时候的A实际已经不是原来的A了

2. 需要注意的是ABA问题不是说CAS操作的过程中A变成了ABA,CAS操作是原子操作,不会被打断。ABA问题场景如下:

先获取了A的值,然后再CAS(A, R), 这时候CAS中的A实际指向的对象的状态可能和它刚获得的时候的状态已经发送了改变。


</pre><pre name="code" class="java">A a = ref.get();
// 根据a的状态做一些操作
// do something
// CAS,这时候会出现ABA问题,a指向的对象可能已经变了
ref.compareAndSet(a, b)


解决ABA问题方法就是给状态设置时间戳,这是并发中加乐观锁的常见做法,如果状态的时间戳发生了改变,证明已经不是原来的对象了,所以操作失败

// 用int做时间戳
AtomicStampedReference<QNode> tail = new AtomicStampedReference<CompositeLock.QNode>(null, 0);
int[] currentStamp = new int[1];
// currentStamp中返回了时间戳信息
QNode tailNode = tail.get(currentStamp);
tail.compareAndSet(tailNode, null, currentStamp[0], currentStamp[0] + 1)

下面我们来看一下java.util.concurrent.atomic.AtomicStampedReference的源代码是如何实现的。

下面代码来自JDK1.7,条理很清晰,实现有几个要点:

1. 创建一个Pair类来记录对象引用和时间戳信息,采用int作为时间戳,实际使用的时候时间戳信息要做成自增的,否则时间戳如果重复,还会出现ABA的问题。这个Pair对象是不可变对象,所有的属性都是final的, of方法每次返回一个新的不可变对象

2. 使用一个volatile类型的引用指向当前的Pair对象,一旦volatile引用发生变化,变化对所有线程可见

3. set方法时,当要设置的对象和当前Pair对象不一样时,新建一个不可变的Pair对象

4. compareAndSet方法中,只有期望对象的引用和版本号和目标对象的引用和版本好都一样时,才会新建一个Pair对象,然后用新建的Pair对象和原理的Pair对象做CAS操作

5. 实际的CAS操作比较的是当前的pair对象和新建的pair对象,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;

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

    public void set(V newReference, int newStamp) {
        Pair<V> current = pair;
        if (newReference != current.reference || newStamp != current.stamp)
            this.pair = Pair.of(newReference, newStamp);
    }

    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 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) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }








你可能感兴趣的:(thread,多线程,并发,乐观锁)