1. 程式人生 > 其它 >java編譯器原始碼詳解_java併發包之AtomicStampedReference原始碼分析(ABA問題詳解)

java編譯器原始碼詳解_java併發包之AtomicStampedReference原始碼分析(ABA問題詳解)

技術標籤:java編譯器原始碼詳解

一、問題

(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: 1thread 2 read value: 1thread 2 update from 1 to 2thread 2 read value: 2thread 2 update from 2 to 1thread 1 update from 1 to 3

四、ABA的危害

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

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

public class ABATest { static class Stack { // 將top放在原子類中 private AtomicReference 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 { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static  Pair of(T reference, int stamp) { return new Pair(reference, stamp); }}

將元素值和版本號繫結在一起,儲存在Pair的reference和stamp(郵票、戳的意思)中。

(二)屬性

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