AtomicStampedReference源碼分析

????歡迎大家搜索“小猴子的技術(shù)筆記”關(guān)注我的公眾號狡逢,領(lǐng)取更多學習資料逻谦。有問題可以及時和我交流刷晋。

????之前的文章已經(jīng)介紹過CAS的操作原理咸产,它雖然能夠保證數(shù)據(jù)的原子性矢否,但還是會有一個ABA的問題。

????那么什么是ABA的問題呢脑溢?假設(shè)有一個共享變量“num”,有個線程A在第一次進行修改的時候把num的值修改成了33僵朗。修改成功之后,緊接著又立刻把“num”的修改回了22屑彻。另外一個線程B再去修改這個值的時候并不能感知到這個值被修改過验庙。


在這里插入圖片描述

????換句話說,別人把你賬戶里面的錢拿出來去投資社牲,在你發(fā)現(xiàn)之前又給你還了回去粪薛,那這個錢還是原來的那個錢嗎?你老婆出軌之后又回到了你身邊搏恤,還是你原來的那個老婆嗎违寿?

????為了模擬ABA的問題让禀,我啟動了兩個線程訪問一個共享的變量。將下面的代碼拷貝到編譯器中陨界,運行進行測試:

public class ABATest {
    private final static AtomicInteger num = new AtomicInteger(100);

    public static void main(String[] args) {
        new Thread(() -> {
            num.compareAndSet(100, 101);
            num.compareAndSet(101, 100);
            System.out.println(Thread.currentThread().getName() + " 修改num之后的值:" + num.get());
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                num.compareAndSet(100, 200);
                System.out.println(Thread.currentThread().getName() + " 修改num之后的值:" + num.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
在這里插入圖片描述

????第一個線程先進行修改把數(shù)值從100修改為101,然后在從101修改回100痛阻,這個過程其實是發(fā)成了ABA的操作菌瘪。第二個線程等待3秒(為了是讓第一個線程執(zhí)行完畢,第二個線程在執(zhí)行)之后進行值從100修改為200阱当。按照我們的理解俏扩,第一個線程已經(jīng)修改過原來的值了,那么第二個線程就不應該修改成功弊添。但是如果你運行下面的測試用例的話录淡,你會發(fā)現(xiàn)它是可以進行修改成功的,請看運行結(jié)果:


Thread-0 修改num之后的值:100
Thread-1 修改num之后的值:200

????雖然結(jié)果是符合我們的預期的:數(shù)值被成功地進行了修改油坝,但是修改的過程卻是不符合我們的預期的嫉戚。

????為了解決這個問題,我們可以在修改的時候附加上一個版本號澈圈,也就是第幾次修改彬檀。每次修改的時候把版本號帶上,如果版本號能夠?qū)纳系脑捑瓦M行修改瞬女,如果對應不上的話就不允許進行修改窍帝。


在這里插入圖片描述

????所以如果修改的時候帶上的版本號不一致的話是不能夠進行成功修改的。我們可以按照上面的原理自己進行版本號的封裝诽偷,但也許會比較麻煩坤学。因此我們可以使用JDK給我們提供的一個已經(jīng)封裝好的類“AtomicStampedReference”來進行我們數(shù)據(jù)的更新。我們來看看下面的這些例子:

public class AtomicStampedReferenceTest {

    private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 第1次版本號:" + stamp.getStamp());
            stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 第2次版本號:" + stamp.getStamp());
            stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 第2次版本號:" + stamp.getStamp());
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " 第1次版本號:" + stamp.getStamp());
                stamp.compareAndSet(100, 400, stamp.getStamp(), stamp.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + " 獲取到的值:" + stamp.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
Thread-0 第1次版本號:1
Thread-0 第2次版本號:2
Thread-0 第2次版本號:2
Thread-1 第1次版本號:2
Thread-1 獲取到的值:200

????也是啟動了兩個線程對共享變量進行修改报慕,但是這次不同的是帶著版本號對共享變量進行的修改深浮。下面將上面的例子進行拆解分析,研究下“AtomicStampedReference”到底為我們做了一些什么卖子。

????首先分析共享變量的創(chuàng)建:構(gòu)建了一個“AtomicStampedReference”對象略号,并且顯示的賦值了100和1。

private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);

????構(gòu)造函數(shù)調(diào)用了下面的源碼:


public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

????"initialRef"是初始值洋闽,也就是我們定義的100玄柠,“initialStamp”是我們顯示聲明的一個整形類型的版本號。只要在int的范圍內(nèi)即可诫舅,但是不要太大了羽利, 畢竟是int如果超了就會丟失精度問題。

????然后調(diào)用了“Pair.of(initialRef, initialStamp)”刊懈,繼續(xù)跟進源碼查看:


在這里插入圖片描述

????通過觀察源碼可以發(fā)現(xiàn)類“Pair”是“AtomicStampedReference”類的一個靜態(tài)內(nèi)部類这弧,有兩個參數(shù)的構(gòu)造函數(shù)娃闲,然后把我們傳遞進來的初始值和版本號進行賦值給“Pair”對象∝依耍可以注意到“pair”被關(guān)鍵字“volatile”修飾皇帮,也就保證了內(nèi)存的可見性和禁止指令的重排序。因此如果“pair”發(fā)生了變化蛋辈,那么所有持有其引用的信息都會進行相應的數(shù)據(jù)更新属拾。


在這里插入圖片描述

????到此為止,“AtomicStampedReference”對象初始化完畢冷溶,內(nèi)部包含了一個“reference”值為100渐白, “stamp”為1的“pair”靜態(tài)內(nèi)部類。

????“stamp.getStamp()”目的是為了獲取當前的版本號逞频,我們在初始化的時候顯示設(shè)置了一個值1纯衍,因此第一次獲取到的版本號就是1。


 public int getStamp() {
    return pair.stamp;
}

????“stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);”是進行第一次CAS更新數(shù)據(jù)苗胀,這次更新的時候就帶著版本號去更新了襟诸。

new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + " 第1次版本號:" + stamp.getStamp());
    stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
    System.out.println(Thread.currentThread().getName() + " 第2次版本號:" + stamp.getStamp());
    stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
    System.out.println(Thread.currentThread().getName() + " 第2次版本號:" + stamp.getStamp());
}).start();

????還記得嗎?之前的CAS比較是需要傳遞一個期望值和更新的值(內(nèi)存中的值基协,底層的方法會給我們封裝好 ):

num.compareAndSet(100, 101);

????而帶著版本號的CAS需要我們傳遞四個值励堡,一個是期望值,一個是更新的值堡掏,還有兩個就是期望的時間戳和需要更新的時間戳:


在這里插入圖片描述

V   expectedReference // 表示預期值
V   newReference,     // 表示要更新的值
int expectedStamp,    // 表示預期的時間戳
int newStamp          // 表示要更新的時間戳

????之后進行了預期值的判斷应结,預期時間戳的判斷,要更新的值和當前的值如果一樣的話泉唁,并且要更新的版本號和當前的版本號一樣的話就返回成功鹅龄。


在這里插入圖片描述

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

????這里我們會發(fā)現(xiàn)在“compareAndSet”方法中最后還調(diào)用了“casPair”方法,從名字就可以看到亭畜,主要是使用CAS機制更新新的值reference和時間戳stamp扮休。而最終調(diào)用的底層是一個本地的方法對數(shù)據(jù)進行的修改。

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

????對于需要自己進行CAS處理的地方拴鸵,我們可以使用“AtomicStampedReference<V>”來進行數(shù)據(jù)的處理玷坠。它既支持泛型,同時還可以避免傳統(tǒng)CAS中ABA的問題劲藐,使數(shù)據(jù)更加安全八堡。

????歡迎大家搜索“小猴子的技術(shù)筆記”關(guān)注我的公眾號,實時更新文章聘芜。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兄渺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汰现,更是在濱河造成了極大的恐慌挂谍,老刑警劉巖叔壤,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異口叙,居然都是意外死亡炼绘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門妄田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饭望,“玉大人,你說我怎么就攤上這事形庭。” “怎么了厌漂?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵萨醒,是天一觀的道長。 經(jīng)常有香客問我苇倡,道長富纸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任旨椒,我火速辦了婚禮晓褪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘综慎。我一直安慰自己涣仿,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布示惊。 她就那樣靜靜地躺著好港,像睡著了一般。 火紅的嫁衣襯著肌膚如雪米罚。 梳的紋絲不亂的頭發(fā)上钧汹,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音录择,去河邊找鬼拔莱。 笑死,一個胖子當著我的面吹牛隘竭,可吹牛的內(nèi)容都是我干的塘秦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼动看,長吁一口氣:“原來是場噩夢啊……” “哼嗤形!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弧圆,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤赋兵,失蹤者是張志新(化名)和其女友劉穎笔咽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霹期,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡叶组,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了历造。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甩十。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吭产,靈堂內(nèi)的尸體忽然破棺而出侣监,到底是詐尸還是另有隱情,我是刑警寧澤臣淤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布橄霉,位于F島的核電站,受9級特大地震影響邑蒋,放射性物質(zhì)發(fā)生泄漏姓蜂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一医吊、第九天 我趴在偏房一處隱蔽的房頂上張望钱慢。 院中可真熱鬧,春花似錦卿堂、人聲如沸束莫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽麦箍。三九已至,卻和暖如春陶珠,著一層夾襖步出監(jiān)牢的瞬間挟裂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工揍诽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诀蓉,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓暑脆,卻偏偏與公主長得像渠啤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子添吗,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容