您的位置:首页 > Web前端

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

2015-08-10 17:08 901 查看
谈论高并发(十一)几个自旋锁的实现(五岁以下儿童)中使用了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);
    }
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: