1. 程式人生 > >死磕 java併發包之AtomicStampedReference原始碼分析(ABA問題詳解)

死磕 java併發包之AtomicStampedReference原始碼分析(ABA問題詳解)

問題

(1)什麼是ABA?

(2)ABA的危害?

(3)ABA的解決方法?

(4)AtomicStampedReference是什麼?

(5)AtomicStampedReference是怎麼解決ABA的?

簡介

AtomicStampedReference是java併發包下提供的一個原子類,它能解決其它原子類無法解決的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 class ABATest {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);

        new Thread(()->{
            int value = atomicInteger.get();
            System.out.println("thread 1 read value: " + value);

            // 阻塞1s
            LockSupport.parkNanos(1000000000L);

            if (atomicInteger.compareAndSet(value, 3)) {
                System.out.println("thread 1 update from " + value + " to 3");
            } else {
                System.out.println("thread 1 update fail!");
            }
        }).start();

        new Thread(()->{
            int value = atomicInteger.get();
            System.out.println("thread 2 read value: " + value);
            if (atomicInteger.compareAndSet(value, 2)) {
                System.out.println("thread 2 update from " + value + " to 2");

                // do sth

                value = atomicInteger.get();
                System.out.println("thread 2 read value: " + value);
                if (atomicInteger.compareAndSet(value, 1)) {
                    System.out.println("thread 2 update from " + value + " to 1");
                }
            }
        }).start();
    }
}

列印結果為:

thread 1 read value: 1
thread 2 read value: 1
thread 2 update from 1 to 2
thread 2 read value: 2
thread 2 update from 2 to 1
thread 1 update from 1 to 3

ABA的危害

為了更好地理解ABA的危害,我們還是來看一個現實點的例子。

假設我們有一個無鎖的棧結構,如下:

public class ABATest {

    static class Stack {
        // 將top放在原子類中
        private AtomicReference<Node> top = new AtomicReference<>();
        // 棧中節點資訊
        static class Node {
            int value;
            Node next;

            public Node(int value) {
                this.value = value;
            }
        }
        // 出棧操作
        public Node pop() {
            for (;;) {
                // 獲取棧頂節點
                Node t = top.get();
                if (t == null) {
                    return null;
                }
                // 棧頂下一個節點
                Node next = t.next;
                // CAS更新top指向其next節點
                if (top.compareAndSet(t, next)) {
                    // 把棧頂元素彈出,應該把next清空防止外面直接操作棧
                    t.next = null;
                    return t;
                }
            }
        }
        // 入棧操作
        public void push(Node node) {
            for (;;) {
                // 獲取棧頂節點
                Node next = top.get();
                // 設定棧頂節點為新節點的next節點
                node.next = next;
                // CAS更新top指向新節點
                if (top.compareAndSet(next, node)) {
                    return;
                }
            }
        }
    }
}

咋一看,這段程式似乎沒有什麼問題,然而試想以下情形。

假如,我們初始化棧結構為 top->1->2->3,然後有兩個執行緒分別做如下操作:

(1)執行緒1執行pop()出棧操作,但是執行到if (top.compareAndSet(t, next)) {這行之前暫停了,所以此時節點1並未出棧;

(2)執行緒2執行pop()出棧操作彈出節點1,此時棧變為 top->2->3;

(3)執行緒2執行pop()出棧操作彈出節點2,此時棧變為 top->3;

(4)執行緒2執行push()入棧操作新增節點1,此時棧變為 top->1->3;

(5)執行緒1恢復執行,比較節點1的引用並沒有改變,執行CAS成功,此時棧變為 top->2;

What?點解變成 top->2 了?不是應該變成 top->3 嗎?

那是因為執行緒1在第一步儲存的next是節點2,所以它執行CAS成功後top節點就指向了節點2了。

測試程式碼如下:

private static void testStack() {
    // 初始化棧為 top->1->2->3
    Stack stack = new Stack();
    stack.push(new Stack.Node(3));
    stack.push(new Stack.Node(2));
    stack.push(new Stack.Node(1));

    new Thread(()->{
        // 執行緒1出棧一個元素
        stack.pop();
    }).start();

    new Thread(()->{
        // 執行緒2出棧兩個元素
        Stack.Node A = stack.pop();
        Stack.Node B = stack.pop();
        // 執行緒2又把A入棧了
        stack.push(A);
    }).start();
}

public static void main(String[] args) {
    testStack();
}

在Stack的pop()方法的if (top.compareAndSet(t, next)) {處打個斷點,執行緒1執行到這裡時阻塞它的執行,讓執行緒2執行完,再執行執行緒1這句,這句執行完可以看到棧的top物件中只有2這個節點了。

記得打斷點的時候一定要打Thread斷點,在IDEA中是右擊選擇Suspend為Thread。

通過這個例子,筆者認為你肯定很清楚ABA的危害了。

ABA的解決方法

ABA的危害我們清楚了,那麼怎麼解決ABA呢?

筆者總結了一下,大概有以下幾種方式:

(1)版本號

比如,上面的棧結構增加一個版本號用於控制,每次CAS的同時檢查版本號有沒有變過。

還有一些資料結構喜歡使用高位儲存一個郵戳來保證CAS的安全。

(2)不重複使用節點的引用

比如,上面的棧結構線上程2執行push()入棧操作的時候新建一個節點傳入,而不是複用節點1的引用;

(3)直接操作元素而不是節點

比如,上面的棧結構push()方法不應該傳入一個節點(Node),而是傳入元素值(int的value)。

好了,扯了這麼多,讓我們來看看java中的AtomicStampedReference是怎麼解決ABA的吧^^

原始碼分析

內部類

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(郵票、戳的意思)中。

屬性

private volatile Pair<V> pair;
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
    objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

宣告一個Pair型別的變數並使用Unsfae獲取其偏移量,儲存到pairOffset中。

構造方法

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 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。

可以看到,java中的實現跟我們上面講的ABA的解決方法是一致的。

首先,使用版本號控制;

其次,不重複使用節點(Pair)的引用,每次都新建一個新的Pair來作為CAS比較的物件,而不是複用舊的;

最後,外部傳入元素值及版本號,而不是節點(Pair)的引用。

案例

讓我們來使用AtomicStampedReference解決開篇那個AtomicInteger帶來的ABA問題。

public class ABATest {

    public static void main(String[] args) {
        testStamp();
    }

    private static void testStamp() {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

        new Thread(()->{
            int[] stampHolder = new int[1];
            int value = atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("thread 1 read value: " + value + ", stamp: " + stamp);

            // 阻塞1s
            LockSupport.parkNanos(1000000000L);

            if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
                System.out.println("thread 1 update from " + value + " to 3");
            } else {
                System.out.println("thread 1 update fail!");
            }
        }).start();

        new Thread(()->{
            int[] stampHolder = new int[1];
            int value = atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
            if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
                System.out.println("thread 2 update from " + value + " to 2");

                // do sth

                value = atomicStampedReference.get(stampHolder);
                stamp = stampHolder[0];
                System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
                if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
                    System.out.println("thread 2 update from " + value + " to 1");
                }
            }
        }).start();
    }
}

執行結果為:

thread 1 read value: 1, stamp: 1
thread 2 read value: 1, stamp: 1
thread 2 update from 1 to 2
thread 2 read value: 2, stamp: 2
thread 2 update from 2 to 1
thread 1 update fail!

可以看到執行緒1最後更新1到3時失敗了,因為這時版本號也變了,成功解決了ABA的問題。

總結

(1)在多執行緒環境下使用無鎖結構要注意ABA問題;

(2)ABA的解決一般使用版本號來控制,並保證資料結構使用元素值來傳遞,且每次新增元素都新建節點承載元素值;

(3)AtomicStampedReference內部使用Pair來儲存元素值及其版本號;

彩蛋

(1)java中還有哪些類可以解決ABA的問題?

AtomicMarkableReference,它不是維護一個版本號,而是維護一個boolean型別的標記,標記值有修改,瞭解一下。

(2)實際工作中遇到過ABA問題嗎?

筆者還真遇到過,以前做棋牌遊戲的時候,ABCD四個玩家,A玩家出了一張牌,然後他這個請求遲遲沒到伺服器,也就是超時了,伺服器就幫他自動出了一張牌。

然後,轉了一圈,又輪到A玩家出牌了,說巧不巧,正好這時之前那個請求到了伺服器,伺服器檢測到現在正好是A出牌,而且請求的也是出牌,就把這張牌打出去了。

然後呢,A玩家的牌就不對了。

最後,我們是通過給每個請求增加一個序列號來處理的,檢測到過期的序列號請求直接拋棄掉。

你有沒有遇到過ABA問題呢?


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

java發包AtomicStampedReference原始碼分析ABA問題

問題 (1)什麼是ABA? (2)ABA的危害? (3)ABA的解決方法? (4)AtomicStampedReference是什麼? (5)AtomicStampedReference是怎麼解決ABA的? 簡介 AtomicStampedReference是java併發包下提供的一個原子類,它能解決其它原子

java發包LongAdder原始碼分析

問題 (1)java8中為什麼要新增LongAdder? (2)LongAdder的實現方式? (3)LongAdder與AtomicLong的對比? 簡介 LongAdder是java8中新增的原子類,在多執行緒環境中,它比AtomicLong效能要高出不少,特別是寫多的場景。 它是怎麼實現的呢?讓我們一起

java同步系列ReentrantLock原始碼解析——公平鎖、非公平鎖

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

java同步系列ReentrantLock原始碼解析——條件鎖

問題 (1)條件鎖是什麼? (2)條件鎖適用於什麼場景? (3)條件鎖的await()是在其它執行緒signal()的時候喚醒的嗎? 簡介 條件鎖,是指在獲取鎖之後發現當前業務場景自己無法處理,而需要等待某個條件的出現才可以繼續處理時使用的一種鎖。 比如,在阻塞佇列中,當佇列中沒有元素的時候是無法彈出一個元素

java發包LongAdder源碼分析

ica sys offset ktr 遷移 對比 .get unsafe join() 問題 (1)java8中為什麽要新增LongAdder? (2)LongAdder的實現方式? (3)LongAdder與AtomicLong的對比? 簡介 LongAdder是java

java同步系列ReentrantReadWriteLock原始碼解析

問題 (1)讀寫鎖是什麼? (2)讀寫鎖具有哪些特性? (3)ReentrantReadWriteLock是怎麼實現讀寫鎖的? (4)如何使用ReentrantReadWriteLock實現高效安全的TreeMap? 簡介 讀寫鎖是一種特殊的鎖,它把對共享資源的訪問分為讀訪問和寫訪問,多個執行緒可以同時對共享

java同步系列Semaphore原始碼解析

問題 (1)Semaphore是什麼? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什麼場景中? (

java同步系列StampedLock原始碼解析

問題 (1)StampedLock是什麼? (2)StampedLock具有什麼特性? (3)StampedLock是否支援可重入

java同步系列CyclicBarrier原始碼解析——有圖有真相

問題 (1)CyclicBarrier是什麼? (2)CyclicBarrier具有什麼特性? (3)CyclicBarrier與

java同步系列Phaser原始碼解析

問題 (1)Phaser是什麼? (2)Phaser具有哪些特性? (3)Phaser相對於CyclicBarrier和Count

java同步系列AQS終篇面試

問題 (1)AQS的定位? (2)AQS的重要組成部分? (3)AQS運用的設計模式? (4)AQS的總體流程? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 在之前的章節中,我們一起學習了ReentrantLock、R

java集合----ArrayList原始碼分析基於jdk1.8

一、ArrayList 1、ArrayList是什麼: ArrayList就是動態陣列,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList介面,靈活的設定陣列的大小等好處,實現了Randomaccess介面,支援快速隨

Java定時任務Timer排程器【一】 原始碼分析圖文

就以鬧鐘的例子開頭吧(後續小節皆以鬧鐘為例,所有原始碼只列關鍵部分)。 public class ScheduleDemo { public static void main(String[] args) throws InterruptedException {

amcl原始碼解析完全

0. 寫在最前面 這篇文章記錄下自己在閱讀amcl原始碼過程中的一些理解,如有不妥,歡迎評論或私信。 本文中所有程式碼因為篇幅等問題,都只給出主要部分,詳細的自己下載下來對照著看。 作者是在校研究生,會長期跟新自己學習ROS以及SLAM過程中的一些理解,喜歡的話歡迎

深入理解Java發包ConcurrentHashMap

【宣告】本部落格大部分內容來自公眾號ImportNew HashMap的容量由負載因子決定,插入的元素超過了容量的範圍就會觸發擴容操作,就是rehash。 在多執行緒環境下,若同時存在其他元素進行put操作,如果hash值相同,可能出現在同一陣列下用連結串列

Java發包Lock鎖和Condition條件

一、Synchronized synchronized是Java的一個關鍵字,也就是Java語言內建的特性,如果一個程式碼塊被synchronized修飾了,當一個執行緒獲取了對應的鎖,執行程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而獲取

java魔法類Unsafe解析

cat 序列化 簡介 分析 img 內部 基本功 圖片 resource 問題 (1)Unsafe是什麽? (2)Unsafe只有CAS的功能嗎? (3)Unsafe為什麽是不安全的? (4)怎麽使用Unsafe? 簡介 本章是java並發包專題的第一章,但是第一篇寫的卻不

java原子類終結篇面試題

static ets new 點擊 比較 原子操作 地址 累加 turn 概覽 原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。 原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割

java同步系列開篇

討論 關註 使用 避免死鎖 更新數據 讀寫 上下文切換 monit 缺點 簡介 同步系列,這是彤哥想了好久的名字,本來是準備寫鎖相關的內容,但是java中的CountDownLatch、Semaphore、CyclicBarrier這些類又不屬於鎖,它們和鎖又有很多共同點,