什么是ABA問題?Java里面的原生解決方案是什么?原理是什么?
AtomicStampedReference是一個帶有時間戳的對象引用,能很好的解決CAS機(jī)制中的ABA問題,這篇文章將通過案例對其介紹分析。
一、ABA問題
ABA問題是CAS機(jī)制中出現(xiàn)的一個問題,他的描述是這樣的。我們直接畫一張圖來演示,
什么意思呢?就是說一個線程把數(shù)據(jù)A變?yōu)榱薆,然后又重新變成了A。此時另外一個線程讀取的時候,發(fā)現(xiàn)A沒有變化,就誤以為是原來的那個A。這就是有名的ABA問題。ABA問題會帶來什么后果呢?我們舉個例子。
一個小偷,把別人家的錢偷了之后又還了回來,還是原來的錢嗎,ABA問題也一樣,如果不好好解決就會帶來大量的問題。最常見的就是資金問題,也就是別人如果挪用了你的錢,在你發(fā)現(xiàn)之前又還了回來。但是別人卻已經(jīng)觸犯了法律。
如何去解決這個ABA問題呢,就是使用今天所說的AtomicStampedReference。
二、AtomicStampedReference
1、問題解決
我們先給出一個ABA的例子,對ABA問題進(jìn)行場景重現(xiàn)。
- public class AtomicTest {
- private static AtomicInteger index = new AtomicInteger(10);
- public static void main(String[] args) {
- new Thread(() -> {
- index.compareAndSet(10, 11);
- index.compareAndSet(11, 10);
- System.out.println(Thread.currentThread().getName()+
- ":10->11->10");
- },"張三").start();
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(2);
- boolean isSuccess = index.compareAndSet(10, 12);
- System.out.println(Thread.currentThread().getName()+
- ":index是預(yù)期的10嘛,"+isSuccess
- +" 設(shè)置的新值是:"+index.get());
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- },"李四").start();
- }
- }
在上面的代碼中,我們使用張三線程,對index10->11->10的變化,然后李四線程讀取index觀察是否有變化,并設(shè)置新值。運(yùn)行一下看看結(jié)果:
這個案例重現(xiàn)了ABA的問題場景,下面我們看如何使用AtomicStampedReference解決這個問題的。
- public class AtomicTest2 {
- private static AtomicInteger index = new AtomicInteger(10);
- static AtomicStampedReference<Integer> stampRef
- = new AtomicStampedReference(10, 1);
- public static void main(String[] args) {
- new Thread(() -> {
- int stamp = stampRef.getStamp();
- System.out.println(Thread.currentThread().getName()
- + " 第1次版本號: " + stamp);
- stampRef.compareAndSet(10, 11,stampRef.getStamp(),stampRef.getStamp()+1);
- System.out.println(Thread.currentThread().getName()
- + " 第2次版本號: " + stampRef.getStamp());
- stampRef.compareAndSet(11, 10,stampRef.getStamp(),stampRef.getStamp()+1);
- System.out.println(Thread.currentThread().getName()
- + " 第3次版本號: " + stampRef.getStamp());
- },"張三").start();
- new Thread(() -> {
- try {
- int stamp = stampRef.getStamp();
- System.out.println(Thread.currentThread().getName()
- + " 第1次版本號: " + stamp);
- TimeUnit.SECONDS.sleep(2);
- boolean isSuccess =stampRef.compareAndSet(10, 12,
- stampRef.getStamp(),stampRef.getStamp()+1);
- System.out.println(Thread.currentThread().getName()
- + " 修改是否成功: "+ isSuccess+" 當(dāng)前版本 :" + stampRef.getStamp());
- System.out.println(Thread.currentThread().getName()
- + " 當(dāng)前實際值: " + stampRef.getReference());
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- },"李四").start();
- }
- }
上面的代碼我們再來分析一下,我們會發(fā)現(xiàn)AtomicStampedReference里面增加了一個時間戳,也就是說每一次修改只需要設(shè)置不同的版本好即可。我們先運(yùn)行一邊看看:
這里使用的是AtomicStampedReference的compareAndSet函數(shù),這里面有四個參數(shù):
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。
(1)第一個參數(shù)expectedReference:表示預(yù)期值。
(2)第二個參數(shù)newReference:表示要更新的值。
(3)第三個參數(shù)expectedStamp:表示預(yù)期的時間戳。
(4)第四個參數(shù)newStamp:表示要更新的時間戳。
這個compareAndSet方法到底是如何實現(xiàn)的,我們深入到源碼中看看。
2、源碼分析
- 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)));
- }
剛剛這四個參數(shù)的意思已經(jīng)說了,我們主要關(guān)注的就是實現(xiàn),首先我們看到的就是這個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);
- }
- }
在這里我們會發(fā)現(xiàn)Pair里面只是包存了值reference和時間戳stamp。
在compareAndSet方法中最后還調(diào)用了casPair方法,從名字就可以看到,主要是使用CAS機(jī)制更新新的值reference和時間戳stamp。我們可以進(jìn)入這個方法中看看。
- //底層調(diào)用的是UNSAFE的compareAndSwapObject方法
- private boolean casPair(Pair<V> cmp, Pair<V> val) {
- return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
- }
三、總結(jié)
其實除了AtomicStampedReference類,還有一個原子類也可以解決,就是AtomicMarkableReference,它不是維護(hù)一個版本號,而是維護(hù)一個boolean類型的標(biāo)記,用法沒有AtomicStampedReference靈活。因此也只是在特定的場景下使用。
本文轉(zhuǎn)載自微信公眾號「愚公要移山」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系愚公要移山公眾號。