1、基本的ABA問題
在CAS算法中,需要取出內存中某時刻的數據(由用戶完成),在下一時刻比較並交換(CPU保證原子操作),這個時間差會導致數據的變化。 假設有以下順序事件: > 1、線程1從內存位置V中取出A > 2、線程2從內存位置V中取出A > 3、線程2進行了寫操作,將B寫入內存位置V > 4、線程2將A再次寫入內存位置V > 5、線程1進行CAS操作,發現V中仍然是A,交換成功盡管線程1的CAS操作成功,但線程1並不知道內存位置V的數據發生過改變
2、ABA問題示例
``` public class ABADemo {private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get());
},"t2").start();
}
}
> * 初始值為100,線程t1將100改成101,然后又將101改回100
> * 線程t2先睡眠1秒,等待t1操作完成,然后t2線程將值改成2019
> * 可以看到,線程2修改成功
輸出結果:
true 修改后的值:2019
<h1>3、ABA問題解決</h1>
要解決ABA問題,可以增加一個版本號,當內存位置V的值每次被修改后,版本號都加1
### AtomicStampedReference
AtomicStampedReference內部維護了對象值和版本號,在創建AtomicStampedReference對象時,需要傳入初始值和初始版本號,
當AtomicStampedReference設置對象值時,對象值以及狀態戳都必須滿足期望值,寫入才會成功
#### 示例
public class ABADemo {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) {
new Thread(() -> {
System.out.println("t1拿到的初始版本號:" + atomicStampedReference.getStamp());
//睡眠1秒,是為了讓t2線程也拿到同樣的初始版本號
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println("t2拿到的初始版本號:" + stamp);
//睡眠3秒,是為了讓t1線程完成ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最新版本號:" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t當前 值:" + atomicStampedReference.getReference());
},"t2").start();
}
}
> 1、初始值100,初始版本號1
> 2、線程t1和t2拿到一樣的初始版本號
> 3、線程t1完成ABA操作,版本號遞增到3
> 4、線程t2完成CAS操作,最新版本號已經變成3,跟線程t2之前拿到的版本號1不相等,操作失敗
輸出結果:
t1拿到的初始版本號:1
t2拿到的初始版本號:1
最新版本號:3
false 當前 值:100
### AtomicMarkableReference
AtomicStampedReference可以給引用加上版本號,追蹤引用的整個變化過程,如:A -> B -> C -> D - > A,通過AtomicStampedReference,我們可以知道,引用變量中途被更改了3次
但是,有時候,我們並不關心引用變量更改了幾次,只是單純的關心是否更改過,所以就有了AtomicMarkableReference
AtomicMarkableReference的唯一區別就是不再用int標識引用,而是使用boolean變量——表示引用變量是否被更改過
#### 示例
public class ABADemo {
private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(100,false);
public static void main(String[] args) {
new Thread(() -> {
System.out.println("t1版本號是否被更改:" + atomicMarkableReference.isMarked());
//睡眠1秒,是為了讓t2線程也拿到同樣的初始版本號
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicMarkableReference.compareAndSet(100, 101,atomicMarkableReference.isMarked(),true);
atomicMarkableReference.compareAndSet(101, 100,atomicMarkableReference.isMarked(),true);
},"t1").start();
new Thread(() -> {
boolean isMarked = atomicMarkableReference.isMarked();
System.out.println("t2版本號是否被更改:" + isMarked);
//睡眠3秒,是為了讓t1線程完成ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("是否更改過:" + atomicMarkableReference.isMarked());
System.out.println(atomicMarkableReference.compareAndSet(100, 2019,isMarked,true) + "\t當前 值:" + atomicMarkableReference.getReference());
},"t2").start();
}
}
> 1、初始值100,初始版本號未被修改 false
> 2、線程t1和t2拿到一樣的初始版本號都未被修改 false
> 3、線程t1完成ABA操作,版本號被修改 true
> 4、線程t2完成CAS操作,版本號已經變成true,跟線程t2之前拿到的版本號false不相等,操作失敗
輸出結果:
t1版本號是否被更改:false
t2版本號是否被更改:false
是否更改過:true
false 當前 值:100