AtomicStampedReference源码分析
之前的文章已經(jīng)介紹過(guò)CAS的操作原理,它雖然能夠保證數(shù)據(jù)的原子性,但還是會(huì)有一個(gè)ABA的問(wèn)題。
????那么什么是ABA的問(wèn)題呢?假設(shè)有一個(gè)共享變量“num”,有個(gè)線程A在第一次進(jìn)行修改的時(shí)候把num的值修改成了33。修改成功之后,緊接著又立刻把“num”的修改回了22。另外一個(gè)線程B再去修改這個(gè)值的時(shí)候并不能感知到這個(gè)值被修改過(guò)。
????換句話說(shuō),別人把你賬戶里面的錢(qián)拿出來(lái)去投資,在你發(fā)現(xiàn)之前又給你還了回去,那這個(gè)錢(qián)還是原來(lái)的那個(gè)錢(qián)嗎?你老婆出軌之后又回到了你身邊,還是你原來(lái)的那個(gè)老婆嗎?
????為了模擬ABA的問(wèn)題,我啟動(dòng)了兩個(gè)線程訪問(wèn)一個(gè)共享的變量。將下面的代碼拷貝到編譯器中,運(yùn)行進(jìn)行測(cè)試:
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();} }
????第一個(gè)線程先進(jìn)行修改把數(shù)值從100修改為101,然后在從101修改回100,這個(gè)過(guò)程其實(shí)是發(fā)成了ABA的操作。第二個(gè)線程等待3秒(為了是讓第一個(gè)線程執(zhí)行完畢,第二個(gè)線程在執(zhí)行)之后進(jìn)行值從100修改為200。按照我們的理解,第一個(gè)線程已經(jīng)修改過(guò)原來(lái)的值了,那么第二個(gè)線程就不應(yīng)該修改成功。但是如果你運(yùn)行下面的測(cè)試用例的話,你會(huì)發(fā)現(xiàn)它是可以進(jìn)行修改成功的,請(qǐng)看運(yùn)行結(jié)果:
????雖然結(jié)果是符合我們的預(yù)期的:數(shù)值被成功地進(jìn)行了修改,但是修改的過(guò)程卻是不符合我們的預(yù)期的。
????為了解決這個(gè)問(wèn)題,我們可以在修改的時(shí)候附加上一個(gè)版本號(hào),也就是第幾次修改。每次修改的時(shí)候把版本號(hào)帶上,如果版本號(hào)能夠?qū)?yīng)的上的話就進(jìn)行修改,如果對(duì)應(yīng)不上的話就不允許進(jìn)行修改。
????所以如果修改的時(shí)候帶上的版本號(hào)不一致的話是不能夠進(jìn)行成功修改的。我們可以按照上面的原理自己進(jìn)行版本號(hào)的封裝,但也許會(huì)比較麻煩。因此我們可以使用JDK給我們提供的一個(gè)已經(jīng)封裝好的類“AtomicStampedReference”來(lái)進(jìn)行我們數(shù)據(jù)的更新。我們來(lái)看看下面的這些例子:
????也是啟動(dòng)了兩個(gè)線程對(duì)共享變量進(jìn)行修改,但是這次不同的是帶著版本號(hào)對(duì)共享變量進(jìn)行的修改。下面將上面的例子進(jìn)行拆解分析,研究下“AtomicStampedReference”到底為我們做了一些什么。
????首先分析共享變量的創(chuàng)建:構(gòu)建了一個(gè)“AtomicStampedReference”對(duì)象,并且顯示的賦值了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”是我們顯示聲明的一個(gè)整形類型的版本號(hào)。只要在int的范圍內(nèi)即可,但是不要太大了, 畢竟是int如果超了就會(huì)丟失精度問(wèn)題。
????然后調(diào)用了“Pair.of(initialRef, initialStamp)”,繼續(xù)跟進(jìn)源碼查看:
????通過(guò)觀察源碼可以發(fā)現(xiàn)類“Pair”是“AtomicStampedReference”類的一個(gè)靜態(tài)內(nèi)部類,有兩個(gè)參數(shù)的構(gòu)造函數(shù),然后把我們傳遞進(jìn)來(lái)的初始值和版本號(hào)進(jìn)行賦值給“Pair”對(duì)象??梢宰⒁獾健皃air”被關(guān)鍵字“volatile”修飾,也就保證了內(nèi)存的可見(jiàn)性和禁止指令的重排序。因此如果“pair”發(fā)生了變化,那么所有持有其引用的信息都會(huì)進(jìn)行相應(yīng)的數(shù)據(jù)更新。
????到此為止,“AtomicStampedReference”對(duì)象初始化完畢,內(nèi)部包含了一個(gè)“reference”值為100, “stamp”為1的“pair”靜態(tài)內(nèi)部類。
????“stamp.getStamp()”目的是為了獲取當(dāng)前的版本號(hào),我們?cè)诔跏蓟臅r(shí)候顯示設(shè)置了一個(gè)值1,因此第一次獲取到的版本號(hào)就是1。
public int getStamp() {return pair.stamp; }????“stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);”是進(jìn)行第一次CAS更新數(shù)據(jù),這次更新的時(shí)候就帶著版本號(hào)去更新了。
new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 第1次版本號(hào):" + stamp.getStamp());stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);System.out.println(Thread.currentThread().getName() + " 第2次版本號(hào):" + stamp.getStamp());stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);System.out.println(Thread.currentThread().getName() + " 第2次版本號(hào):" + stamp.getStamp()); }).start();????還記得嗎?之前的CAS比較是需要傳遞一個(gè)期望值和更新的值(內(nèi)存中的值,底層的方法會(huì)給我們封裝好 ):
num.compareAndSet(100, 101);- 1
????而帶著版本號(hào)的CAS需要我們傳遞四個(gè)值,一個(gè)是期望值,一個(gè)是更新的值,還有兩個(gè)就是期望的時(shí)間戳和需要更新的時(shí)間戳:
????之后進(jìn)行了預(yù)期值的判斷,
總結(jié)
以上是生活随笔為你收集整理的AtomicStampedReference源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 原子性 atomic 类用法
- 下一篇: JVM启动参数