談論高併發(十二)分析java.util.concurrent.atomic.AtomicStampedReference看看如何解決源代碼CAS的ABA問題

談論高併發(十一)幾個自旋鎖的實現(五歲如下兒童)中使用了java.util.concurrent.atomic.AtomicStampedReference原子變量指向工做隊列的隊尾,爲什麼使用AtomicStampedReference原子變量而不是使用AtomicReference是因爲這個實現中等待隊列的同一個節點具有不一樣的狀態,而同一個節點會屢次進出工做隊列,這就有可能出現出現ABA問題。java


熟悉併發編程的同窗應該知道CAS操做存在ABA問題。咱們先看下CAS操做。編程

CAS(Compare and Swap) 比較並交換操做是一個三元操做: 目標地址的值T(arget)。指望值E(xpected),實際值R(eal),併發

1. 僅僅有當目標值T == 指望值E時。纔會把目標值T設置爲實際值R,不然不改變目標值高併發

2. 不管目標值是否改變,都返回以前的目標值Tthis


相似例如如下的邏輯:atom

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問題就來了:spa

1. 由於CAS僅僅是值比較,比方目標是A, 指望值也是A, 那麼CAS操做會成功。但是這時候目標A可能不是原來的那個A了。它多是A變成了B,再變成了A。.net

因此叫ABA問題,很是形象。線程

ABA問題可能會使程序出錯。比方限時有界隊列鎖中的節點有幾個狀態。儘管引用值是A。但是可能對象的狀態已經變了,這時候的A實際已經不是原來的A了code

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);
    }
相關文章
相關標籤/搜索