(1)什么是ABA?
成都創(chuàng)新互聯(lián)主要從事網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)嘉陵,十載網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專(zhuān)業(yè),歡迎來(lái)電咨詢建站服務(wù):18980820575
(2)ABA的危害?
(3)ABA的解決方法?
(4)AtomicStampedReference是什么?
(5)AtomicStampedReference是怎么解決ABA的?
AtomicStampedReference是java并發(fā)包下提供的一個(gè)原子類(lèi),它能解決其它原子類(lèi)無(wú)法解決的ABA問(wèn)題。
ABA問(wèn)題發(fā)生在多線程環(huán)境中,當(dāng)某線程連續(xù)讀取同一塊內(nèi)存地址兩次,兩次得到的值一樣,它簡(jiǎn)單地認(rèn)為“此內(nèi)存地址的值并沒(méi)有被修改過(guò)”,然而,同時(shí)可能存在另一個(gè)線程在這兩次讀取之間把這個(gè)內(nèi)存地址的值從A修改成了B又修改回了A,這時(shí)還簡(jiǎn)單地認(rèn)為“沒(méi)有修改過(guò)”顯然是錯(cuò)誤的。
比如,兩個(gè)線程按下面的順序執(zhí)行:
(1)線程1讀取內(nèi)存位置X的值為A;
(2)線程1阻塞了;
(3)線程2讀取內(nèi)存位置X的值為A;
(4)線程2修改內(nèi)存位置X的值為B;
(5)線程2修改又內(nèi)存位置X的值為A;
(6)線程1恢復(fù),繼續(xù)執(zhí)行,比較發(fā)現(xiàn)還是A把內(nèi)存位置X的值設(shè)置為C;
可以看到,針對(duì)線程1來(lái)說(shuō),第一次的A和第二次的A實(shí)際上并不是同一個(gè)A。
ABA問(wèn)題通常發(fā)生在無(wú)鎖結(jié)構(gòu)中,用代碼來(lái)表示上面的過(guò)程大概就是這樣:
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();
}
}
打印結(jié)果為:
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的危害,我們還是來(lái)看一個(gè)現(xiàn)實(shí)點(diǎn)的例子。
假設(shè)我們有一個(gè)無(wú)鎖的棧結(jié)構(gòu),如下:
public class ABATest {
static class Stack {
// 將top放在原子類(lèi)中
private AtomicReference top = new AtomicReference<>();
// 棧中節(jié)點(diǎn)信息
static class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
}
// 出棧操作
public Node pop() {
for (;;) {
// 獲取棧頂節(jié)點(diǎn)
Node t = top.get();
if (t == null) {
return null;
}
// 棧頂下一個(gè)節(jié)點(diǎn)
Node next = t.next;
// CAS更新top指向其next節(jié)點(diǎn)
if (top.compareAndSet(t, next)) {
// 把棧頂元素彈出,應(yīng)該把next清空防止外面直接操作棧
t.next = null;
return t;
}
}
}
// 入棧操作
public void push(Node node) {
for (;;) {
// 獲取棧頂節(jié)點(diǎn)
Node next = top.get();
// 設(shè)置棧頂節(jié)點(diǎn)為新節(jié)點(diǎn)的next節(jié)點(diǎn)
node.next = next;
// CAS更新top指向新節(jié)點(diǎn)
if (top.compareAndSet(next, node)) {
return;
}
}
}
}
}
咋一看,這段程序似乎沒(méi)有什么問(wèn)題,然而試想以下情形。
假如,我們初始化棧結(jié)構(gòu)為 top->1->2->3,然后有兩個(gè)線程分別做如下操作:
(1)線程1執(zhí)行pop()出棧操作,但是執(zhí)行到if (top.compareAndSet(t, next)) {
這行之前暫停了,所以此時(shí)節(jié)點(diǎn)1并未出棧;
(2)線程2執(zhí)行pop()出棧操作彈出節(jié)點(diǎn)1,此時(shí)棧變?yōu)?top->2->3;
(3)線程2執(zhí)行pop()出棧操作彈出節(jié)點(diǎn)2,此時(shí)棧變?yōu)?top->3;
(4)線程2執(zhí)行push()入棧操作添加節(jié)點(diǎn)1,此時(shí)棧變?yōu)?top->1->3;
(5)線程1恢復(fù)執(zhí)行,比較節(jié)點(diǎn)1的引用并沒(méi)有改變,執(zhí)行CAS成功,此時(shí)棧變?yōu)?top->2;
What?點(diǎn)解變成 top->2 了?不是應(yīng)該變成 top->3 嗎?
那是因?yàn)榫€程1在第一步保存的next是節(jié)點(diǎn)2,所以它執(zhí)行CAS成功后top節(jié)點(diǎn)就指向了節(jié)點(diǎn)2了。
測(cè)試代碼如下:
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出棧一個(gè)元素
stack.pop();
}).start();
new Thread(()->{
// 線程2出棧兩個(gè)元素
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)) {
處打個(gè)斷點(diǎn),線程1運(yùn)行到這里時(shí)阻塞它的執(zhí)行,讓線程2執(zhí)行完,再執(zhí)行線程1這句,這句執(zhí)行完可以看到棧的top對(duì)象中只有2這個(gè)節(jié)點(diǎn)了。
記得打斷點(diǎn)的時(shí)候一定要打Thread斷點(diǎn),在IDEA中是右擊選擇Suspend為T(mén)hread。
通過(guò)這個(gè)例子,筆者認(rèn)為你肯定很清楚ABA的危害了。
ABA的危害我們清楚了,那么怎么解決ABA呢?
筆者總結(jié)了一下,大概有以下幾種方式:
(1)版本號(hào)
比如,上面的棧結(jié)構(gòu)增加一個(gè)版本號(hào)用于控制,每次CAS的同時(shí)檢查版本號(hào)有沒(méi)有變過(guò)。
還有一些數(shù)據(jù)結(jié)構(gòu)喜歡使用高位存儲(chǔ)一個(gè)郵戳來(lái)保證CAS的安全。
(2)不重復(fù)使用節(jié)點(diǎn)的引用
比如,上面的棧結(jié)構(gòu)在線程2執(zhí)行push()入棧操作的時(shí)候新建一個(gè)節(jié)點(diǎn)傳入,而不是復(fù)用節(jié)點(diǎn)1的引用;
(3)直接操作元素而不是節(jié)點(diǎn)
比如,上面的棧結(jié)構(gòu)push()方法不應(yīng)該傳入一個(gè)節(jié)點(diǎn)(Node),而是傳入元素值(int的value)。
好了,扯了這么多,讓我們來(lái)看看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);
}
}
將元素值和版本號(hào)綁定在一起,存儲(chǔ)在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", AtomicStampedReference.class);
聲明一個(gè)Pair類(lèi)型的變量并使用Unsfae獲取其偏移量,存儲(chǔ)到pairOffset中。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
構(gòu)造方法需要傳入初始值及初始版本號(hào)。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 獲取當(dāng)前的(元素值,版本號(hào))對(duì)
Pair current = pair;
return
// 引用沒(méi)變
expectedReference == current.reference &&
// 版本號(hào)沒(méi)變
expectedStamp == current.stamp &&
// 新引用等于舊引用
((newReference == current.reference &&
// 新版本號(hào)等于舊版本號(hào)
newStamp == current.stamp) ||
// 構(gòu)造新的Pair對(duì)象并CAS更新
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair cmp, Pair val) {
// 調(diào)用Unsafe的compareAndSwapObject()方法CAS更新pair的引用為新引用
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
(1)如果元素值和版本號(hào)都沒(méi)有變化,并且和新的也相同,返回true;
(2)如果元素值和版本號(hào)都沒(méi)有變化,并且和新的不完全相同,就構(gòu)造一個(gè)新的Pair對(duì)象并執(zhí)行CAS更新pair。
可以看到,java中的實(shí)現(xiàn)跟我們上面講的ABA的解決方法是一致的。
首先,使用版本號(hào)控制;
其次,不重復(fù)使用節(jié)點(diǎn)(Pair)的引用,每次都新建一個(gè)新的Pair來(lái)作為CAS比較的對(duì)象,而不是復(fù)用舊的;
最后,外部傳入元素值及版本號(hào),而不是節(jié)點(diǎn)(Pair)的引用。
讓我們來(lái)使用AtomicStampedReference解決開(kāi)篇那個(gè)AtomicInteger帶來(lái)的ABA問(wèn)題。
public class ABATest {
public static void main(String[] args) {
testStamp();
}
private static void testStamp() {
AtomicStampedReference 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();
}
}
運(yùn)行結(jié)果為:
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時(shí)失敗了,因?yàn)檫@時(shí)版本號(hào)也變了,成功解決了ABA的問(wèn)題。
(1)在多線程環(huán)境下使用無(wú)鎖結(jié)構(gòu)要注意ABA問(wèn)題;
(2)ABA的解決一般使用版本號(hào)來(lái)控制,并保證數(shù)據(jù)結(jié)構(gòu)使用元素值來(lái)傳遞,且每次添加元素都新建節(jié)點(diǎn)承載元素值;
(3)AtomicStampedReference內(nèi)部使用Pair來(lái)存儲(chǔ)元素值及其版本號(hào);
(1)java中還有哪些類(lèi)可以解決ABA的問(wèn)題?
AtomicMarkableReference,它不是維護(hù)一個(gè)版本號(hào),而是維護(hù)一個(gè)boolean類(lèi)型的標(biāo)記,標(biāo)記值有修改,了解一下。
(2)實(shí)際工作中遇到過(guò)ABA問(wèn)題嗎?
筆者還真遇到過(guò),以前做×××的時(shí)候,ABCD四個(gè)玩家,A玩家出了一張牌,然后他這個(gè)請(qǐng)求遲遲沒(méi)到服務(wù)器,也就是超時(shí)了,服務(wù)器就幫他自動(dòng)出了一張牌。
然后,轉(zhuǎn)了一圈,又輪到A玩家出牌了,說(shuō)巧不巧,正好這時(shí)之前那個(gè)請(qǐng)求到了服務(wù)器,服務(wù)器檢測(cè)到現(xiàn)在正好是A出牌,而且請(qǐng)求的也是出牌,就把這張牌打出去了。
然后呢,A玩家的牌就不對(duì)了。
最后,我們是通過(guò)給每個(gè)請(qǐng)求增加一個(gè)序列號(hào)來(lái)處理的,檢測(cè)到過(guò)期的序列號(hào)請(qǐng)求直接拋棄掉。
你有沒(méi)有遇到過(guò)ABA問(wèn)題呢?
歡迎關(guān)注我的公眾號(hào)“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。