????歡迎大家搜索“小猴子的技術(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)注我的公眾號,實時更新文章聘芜。