使用AtomicStampedReference解決ABA問題時的坑(AtomicStampedReference<Integer>修改始終不成功)

多線程模型:

image.png

每個線程都有自己的獨立內(nèi)存空間,當線程需要操作主內(nèi)存中的數(shù)據(jù),需要先拷貝一份數(shù)據(jù)到自己的內(nèi)存空間,然后進行修改,再刷回主內(nèi)存
CAS:
CompareAndSwap,比較然后替換,多線程解決數(shù)據(jù)錯亂的方案,如上圖,試想,i=10表示10本書,t1,t2線程同時進行銷售書,如果某一時間,t1和t2線程同時將i=10拷貝到自己的數(shù)據(jù)空間,t1將書減1,i=9再刷回主內(nèi)存,即現(xiàn)在書的數(shù)目已經(jīng)為9了,然后t2線程將的工作內(nèi)存還為10它也進行減1然后刷回主內(nèi)存,這樣主內(nèi)存的i值還是為9,這樣就導(dǎo)致了已經(jīng)賣了兩本書,但是書的數(shù)目卻只減少了1,CAS便是在t2線程進行減了操作后,需要刷回主內(nèi)存的時候,將取到的值與主內(nèi)存的值作比較,相同再進行設(shè)置,不同則設(shè)置失敗。
ABA:
有了CAS可以保證操作數(shù)目一致,但是也出現(xiàn)一個問題,例如,t1線程先將書的數(shù)目減了1然后別人退還一本書,書的數(shù)目又加了1,也就是數(shù)的數(shù)目經(jīng)歷了10->9->10,然后t2線程操作時,發(fā)現(xiàn)主內(nèi)存還是為10,所以覺得沒問題就進行操作了,可能這個例子不能很好的描述問題,但是在某些案例中,會給人一種貍貓換太子的感覺,貼一個圖
image.png

圖片來自:https://baijiahao.baidu.com/s?id=1648077822185803003&wfr=spider&for=pc

一個小偷,把別人家的錢偷了之后又還了回來,還是原來的錢嗎,你老婆出軌之后又回來,還是原來的老婆嗎?ABA問題也一樣,如果不好好解決就會帶來大量的問題。最常見的就是資金問題,也就是別人如果挪用了你的錢,在你發(fā)現(xiàn)之前又還了回來。但是別人卻已經(jīng)觸犯了法律。
解決方案:

AtomicStampedReference

主要思想:通過加版本控制來進行修改,代碼如下:

public class AtomicStampedReferenceTest {

// 初始化,值為100,版本為1
    static AtomicStampedReference<Integer> integerReference =
            new AtomicStampedReference<>(100,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(100, 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,100,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保證t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(100,500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
  • 代碼解析:
    1. 首先t1,t2線程同時拿到對象的最初版本1,值為100
      2.t2 線程等待3秒,保證t1線程將值修改為101,然后再將值修改為100,但是版本已經(jīng)從,1->2->3
      3.t2線程根據(jù)3秒前拿到的版本進行修改,即使數(shù)據(jù)值都為100,但是3秒前的版本是1,現(xiàn)在的版本為3,所以修改失敗
      打印輸出:

t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:true
t1第二次版本:2
t1第一次修改成功?:true
t2修改成功?:false
期望版本是:1--當前版本是:3

問題:

一切看上去好像都沒什么問題,但是當我們把初始值改成大于127的數(shù)值,代碼如下,修改的地方已經(jīng)標注,其實只是將里面的100全部替換為400

public class AtomicStampedReferenceTest {

    static AtomicStampedReference<Integer> integerReference =
            // 將100改成400
            new AtomicStampedReference<>(400,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 將100改成400
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(400, 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,400,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保證t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //將100改成400
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(400,500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

打印輸出

t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:false
t1第二次版本:1
t1第一次修改成功?:false
t2修改成功?:false
t2:期望版本是:1--當前版本是:1

???怎么會呢?我賣100本書讓我賣,我賣400本書就不讓我賣了?
看看方法

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            // 這一行重點,期望值當前值對比,也就是我們傳的100和400
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
  • 上面標注的一行為原因所在,這里需要將兩個Integer對象進行對比,相等則true否則false,那為啥我們傳100就可以400就不行,原因就是Integer的緩存,先看下面的代碼與打印輸出
        Integer a =127;
        Integer b =127;
        System.out.println(a==b); // 輸出true
        a=128;
        b=128;
        System.out.println(a==b);// 輸出false
        a=-127;
        b=-127;
        System.out.println(a==b);// 輸出true
        a=-128;
        b=-128;
        System.out.println(a==b);// 輸出true
        a=-129;
        b=-129;
        System.out.println(a==b);// 輸出false

主要原因就是Integer的緩存范圍為:[-128,127],Integer用==進行值比較,什么時候相等,什么時候不等?
,即只要你的數(shù)值在這個范圍內(nèi)使用上面的方法都沒錯,但是超過了,則每次修改就會失敗

解決方案:使用integer來接收當前的對象,在當前對象的基礎(chǔ)上進行修改,即使用100與400的地方都改成通過integerReference.getReference()直接獲取當前對象,如果直接傳400就會導(dǎo)致Integer裝包時創(chuàng)建新的對象,導(dǎo)致數(shù)值雖然相等,但是對象不相等。

public class AtomicStampedReferenceTest {

    static AtomicStampedReference<Integer> integerReference =
            new AtomicStampedReference<>(400,1);

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            int stamp =  integerReference.getStamp();
            System.out.println("t1第一次版本:"+stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(), 101,
                    stamp, stamp + 1));
            stamp = integerReference.getStamp();
            System.out.println("t1第二次版本:"+stamp);
            System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),400,
                    stamp,stamp+1));

        },"t1");
        Thread t2 = new Thread(() -> {
            int stamp = integerReference.getStamp();
            System.out.println("t2第一次版本:"+stamp);
//            睡眠3秒保證t1完成一次ABA操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),500,
                    stamp,stamp+1));
            System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容