18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?
老王:小陳啊,上一章我們說了AtomicInteger、AtomicBoolean的底層原理,這一篇我們就來說說Atomic系列的另一個分類AtomicReference和AtomicStampReference。
小陳:老王啊,我有個疑問啊,java不是提供了AtomicInteger、AtomicBoolean這些原子類了嗎?為什么還需要有AtomicReference這東西啊?
老王:JUC雖然提供了AtomicInteger、AtomicBoolean這些基本類型的原子類,但是啊有些場景并不是僅僅修改一個變量那么簡單,有可能某個需要修改幾個變量,但是需要這個操作具有原子性,比如說我給你舉例的這個例子:
(1)假如有三個變量,value1、value2、value3,我需要他們都兩兩相等
(2)這時將value1、value2、value3都聲明成AtomicInteger原子類
(3)定義一個線程類,創建兩個線程實例,每個都執行5000次value1、value2、value3的操作
(4)每次操作完成之后對比value1、value2、value3是否兩兩相等,如果不滿足,則打印報錯
public class MultiUpdateDemo {// 聲明三個AtomicInteger的原子類private static AtomicInteger value1 = new AtomicInteger(0);private static AtomicInteger value2 = new AtomicInteger(0);private static AtomicInteger value3 = new AtomicInteger(0);// 定義一個線程,執行3個AtomicInteger的++操作public static class MultiUpdateThread extends Thread{public void run() {for (int i = 0; i < 5000; i++) {value1.incrementAndGet();value2.incrementAndGet();value3.incrementAndGet();// 假如說執行完一次操作之后,出現// value1、value2、value3任何兩兩不相等的情況// 則打印報錯if (value1.get() != value2.get() || value1 != value3|| value2.get() != value3.get()) {System.out.println("不好意思,出錯了!!!!!!");}}}}public static void main(String[] args) throws InterruptedException {// 創建兩個線程,并發的操作MultiUpdateThread thread1 = new MultiUpdateThread();MultiUpdateThread thread2 = new MultiUpdateThread();thread1.start();thread2.start();thread1.join();thread2.join();} }具體得到的實驗結果如下:
老王:根據樣例的實驗結果啊,出現和很多次value1、value2、value3不相等的情況,也就是我們需要的目的沒有達到,小陳啊,你知道這是什么原因嗎?
小陳:老王啊,我想了一下單獨對value1、value2、value3中任意一個執行incrementAndGet是原子的;但是value1.incrementAndGet()、 value2.incrementAndGet()、value3.incrementAndGet()這三個操作合起來就不是原子的。
可能thread1執行value1.incrementAndGet()操作的時候,thread2已經將三個自增操作執行完了,所以啊,thread1和thread2會相互干擾......
老王:哈哈,小陳啊,看來我沒看錯你啊,挺聰明的哦。像是這種情況啊要對多個變量進行操作,同時又要保證這個操作具有原子性,單獨使用AtomicInteger、AtomicBoolean是做不到的。
小陳:老王,如果使用到鎖不可以嗎,比如我可以將上面的幾個操作放到synchronized代碼塊里面:
// lock鎖對象是一個共享變量synchronized(lock) {value1.incrementAndGet();value2.incrementAndGet();value3.incrementAndGet();// 加入說執行完一次操作之后,出現value1、value2、value3任何兩兩不相等的情況if (value1.get() != value2.get() || value1 != value3|| value2.get() != value3.get()) {System.out.println("不好意思,出錯了!!!!!!");} }老王:這種情況下使用synchronized是可以保證原子性的,但是使用到鎖啊,那并發性能就下降了很多了,因為在競爭激烈的時候可能會導致很多線程獲取不到鎖而掛起,那開銷就大了,這個我們在之前的synchronized的重量級鎖章節里面專門分析過了。
小陳:哦哦,原來是這樣啊......
老王:嗯嗯,AtomicIntegter只能確保自己本身操作具有原子性,但是多個AtomicInteger操作合起來這個是確保不了的;可以使用synchronized將多個操作包含起來,但是使用到synchronized的鎖操作勢必會降低一部分并發的性能。
小陳:那怎樣在不使用鎖的情況下保證多個變量的修改是具有原子性的呢?
老王:哈哈,這個時候就需要用到Atomic給我們提供的另外一個類了,AtomicReference。它可以將多個變量封裝為對象的多個屬性,然后一次性的更新整個對象,就能cas的更新多個變量,確保原子性。
AtomicReference實現一個對象原子更新
public class ReferenceDemo {// 聲明一個AtomicReference,封裝Demo對象的private static AtomicReference<Demo> reference = new AtomicReference(new Demo());// 將value1、value2、value3封裝為Demo對象的屬性public static class Demo {public int value1 = 0;public int value2 = 0;public int value3 = 0;}// 創建線程累專門執行對象的更新public static class ReferenceThread extends Thread {public void run() {for (int i = 0; i < 5000; i++) {Demo expected;Demo update;// 直到CAS更新操作成功才退出do {expected = reference.get();update = new Demo();update.value1 = expected.value1 + 1;update.value2 = expected.value2 + 1;update.value3 = expected.value2 + 1;} while (!reference.compareAndSet(expected, update));// 獲取CAS之后的最新對象Demo curDemo = reference.get();// 如果value1、value2、value3中有任意一個不相等,打印報錯if (curDemo.value1 != curDemo.value2 || curDemo.value2 != curDemo.value3|| curDemo.value1 != curDemo.value3) {System.out.println("不好意思,出錯了!!!!!!");}}}}public static void main(String[] args) throws InterruptedException {// 創建兩個線程,并發的操作,驗證并發操作的原子性ReferenceThread thread1 = new ReferenceThread();ReferenceThread thread2 = new ReferenceThread();thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("運行結束了......");}實測結果如下:
并沒有打印報錯信息。它這里啊相當于把value1、value2、value3的更新操作變為了對象的更新操作,這樣原本的3次操作就變為了一次CAS操作,這樣就能保證原子性了。
小陳:原來是這樣啊,原來是多個數據變更的操作變為一個對象變更操作;由于AtomicReference提供了對象替換的CAS操作,所以上面的操作就具有原子性了。
老王:是的,就是這個道理,畫個圖來解析它的步驟,就是這樣的:
(1)將多個變量封裝在一個對象中,比如demo對象,封裝了value1、value2、value3變量的值,此時三個變量均為0
(2)此時要將3個變量的值均更新為1,則新創建一個對象update封裝value1、value2、value3的值均為1
(3)此時只需要將舊的demo對象通過cas操作替換為新的update對象即可,這樣就將多個變量的更新操作變為了一個對象的cas替換操作。
老王:讓我們繼續,來看看AtomicReference底層有什么東西?
AtomicReference原子類底層剖析
首先看一下AtomicReference的內部屬性:
public class AtomicReference<V> implements java.io.Serializable {// unsafe對象private static final Unsafe unsafe = Unsafe.getUnsafe();// 一個泛型對象private volatile V value;// value對象在AtomicReference內部的偏移量private static final long valueOffset;static {try {// 獲取value相對AtomicReference的內部偏移量valueOffset = unsafe.objectFieldOffset(AtomicReference.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }} }看下compareAndSet方法的內部源碼:
public final boolean compareAndSet(V expect, V update) {return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }看樣子跟Atomicinteger和AtomicBoolean原理是一樣的,只不過AtomicInteger、AtomicBoolean底層調用的是unsafe.compareAndSwapInt方法CAS操作int的值,而這里是compareAndSwapObject是CAS操作一個內存對象而已,沒啥大區別。
小陳:看樣子這個AtomicReference也是挺簡單的呀,唯一與AtomicInteger、AtomicBoolean不同的是:
AtomicInteger、AtomicBoolean 執行的是unsafe的compareAndSwapInt方法,在內存層次是直接替換一個int變量的值;然而使用AtomicRefernce你可以創建一個新的對象,將所有的數據變更操作放到新對象里面,然后底層調用unsafe.compareAndSwapObject方法直接替換成新對象啊。
老王:哈哈,看來你把AtomicReference理解透了啊?也是,本來就不難嘛,就是把多個修改放在對象里面,直接CAS替換對象就是了
老王:小陳啊,學到了這里,你對CAS的理解也差不多了,可以算的是深入了,但是你知道CAS操作會有什么問題嗎?
小陳:這個啊,我經常看到一些網站上說CAS操作不可避免的問題之一就是ABA問題?
老王:那你知道什么是ABA問題?
小陳:老王啊,我畫個圖說一下我對ABA問題的理解吧,是這樣的:
(1)線程1要執行CAS操作前,讀取value最新的值為A
(2)然后線程2在這期間將內存value的數據修改成B,然后又修改回了A;
(3)但是線程A不知道,執行CAS操作的時候發現值還是A,以為沒人修改過value的值,也是就執行執行CAS操作成功了
老王:那應該怎么避免ABA這種問題?
小陳:這個應該是多增加一個維度,比如版本號,每一次修改數據版本號則遞增1,然后執行CAS操作的時候多一個版本號維度判斷,這樣就能避免ABA問題了。
老王:是的,確實是需要多一個版本號維度去判斷,那你知道Atomic原子類里面哪個類能解決這個問題嗎?
小陳:啊,這個......我就不知道啊,還是老王你來講講吧。
老王:哈哈,好。Atomic原子類系列里面有一個類叫做AtomicStampedReference,是AtomicReference的升級版本,看名字你就知道多了一個叫做Stamped的東西,這東西就是版本號,也叫作郵戳。下面讓我們看看AtomicStampedReference的內部結構和核心方法。
AtomicStampedReference
內部結構:
public class AtomicStampedReference<V> {// 將當前對象引用和修改的版本號綁定成一個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);}}private volatile Pair<V> pair; }這里比較上面的AtomicReference多了一個stamp版本號,將對象和版本號綁定在一起,形成一對pair,比較的時候同時比較對象的引用和版本號,避免ABA問題。
核心執行修改的CAS方法:
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))); }(1)獲取舊的對象引用expectedRefenence
(2)執行CAS操作前,獲取當前內存最新的數據
(3)對比舊的對象和當前對象的reference引用是否同一個,版本號stamp是否相同
(4)如果相同執行CAS操作替換,否則不一樣說明有別的線程修改過數據,CAS操作失敗
casPair方法:
直接調用底層的unsafe類的compareAndSwapObject方法直接替換一個對象:
private boolean casPair(Pair<V> cmp, Pair<V> val) {return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }讓我們再畫一張圖來捋一捋:
老王:小陳啊,AtomicStampedReference的底層原理通過上面的代碼和畫圖講解,你聽懂了沒?
小陳:某問題了,其實就是比AtomicReference多了一個版本號stamped,在執行CAS操作之前對比reference的值的同時也對比版本號,如果reference一樣但是stamped不一樣,說明期間有人修改過但是又把值改回來了,就不允許執行CAS操作了,這樣就能解決ABA的問題了。
老王:沒錯,理解完全正確。
小陳:老王啊,接下來我們學習什么?
老王:接下來啊,我們講解Atomic系列中的下一章《Atomic系列之LongAdder的底層原理(分段鎖提升并發性能)》
小陳:牛逼plus......,我們下一章見。
關注小陳,公眾號上更多更全的文章
JAVA并發文章目錄(公眾號)
JAVA并發專題 《筑基篇》
1.什么是CPU多級緩存模型?
2.什么是JAVA內存模型?
3.線程安全之可見性、有序性、原子性是什么?
4.什么是MESI緩存一致性協議?怎么解決并發的可見性問題?
JAVA并發專題《練氣篇》
5.volatile怎么保證可見性?
6.什么是內存屏障?具有什么作用?
7.volatile怎么通過內存屏障保證可見性和有序性?
8.volatile為啥不能保證原子性?
9.synchronized是個啥東西?應該怎么使用?
10.synchronized底層之monitor、對象頭、Mark Word?
11.synchronized底層是怎么通過monitor進行加鎖的?
12.synchronized的鎖重入、鎖消除、鎖升級原理?無鎖、偏向鎖、輕量級鎖、自旋、重量級鎖
13.synchronized怎么保證可見性、有序性、原子性?
JAVA并發專題《結丹篇》
14. JDK底層Unsafe類是個啥東西?
15.unsafe類的CAS是怎么保證原子性的?
16.Atomic原子類體系講解
17.AtomicInteger、AtomicBoolean的底層原理
18.AtomicReference、AtomicStampReference底層原理
19.Atomic中的LongAdder底層原理之分段鎖機制
20.Atmoic系列Strimped64分段鎖底層實現源碼剖析
JAVA并發專題《金丹篇》
21.AQS是個啥?為啥說它是JAVA并發工具基礎框架?
22.基于AQS的互斥鎖底層源碼深度剖析
23.基于AQS的共享鎖底層源碼深度剖析
24.ReentrantLock是怎么基于AQS實現獨占鎖的?
25.ReentrantLock的Condition機制底層源碼剖析
26.CountDownLatch 門栓底層源碼和實現機制深度剖析
27.CyclicBarrier 柵欄底層源碼和實現機制深度剖析
28.Semaphore 信號量底層源碼和實現機深度剖析
29.ReentrantReadWriteLock 讀寫鎖怎么表示?
30. ReentrantReadWriteLock 讀寫鎖底層源碼和機制深度剖析
JAVA并發專題《元神篇》并發數據結構篇
31.CopyOnAarrayList 底層分析,怎么通過寫時復制副本,提升并發性能?
32.ConcurrentLinkedQueue 底層分析,CAS 無鎖化操作提升并發性能?
33.ConcurrentHashMap詳解,底層怎么通過分段鎖提升并發性能?
34.LinkedBlockedQueue 阻塞隊列怎么通過ReentrantLock和Condition實現?
35.ArrayBlockedQueued 阻塞隊列實現思路竟然和LinkedBlockedQueue一樣?
36.DelayQueue 底層源碼剖析,延時隊列怎么實現?
37.SynchronousQueue底層原理解析
JAVA并發專題《飛升篇》線程池底層深度剖析
38. 什么是線程池?看看JDK提供了哪些默認的線程池?底層竟然都是基于ThreadPoolExecutor的?
39.ThreadPoolExecutor 構造函數有哪些參數?這些參數分別表示什么意思?
40.內部有哪些變量,怎么表示線程池狀態和線程數,看看道格.李大神是怎么設計的?
41. ThreadPoolExecutor execute執行流程?怎么進行任務提交的?addWorker方法干了啥?什么是workder?
42. ThreadPoolExecutor execute執行流程?何時將任務提交到阻塞隊列? 阻塞隊列滿會發生什么?
43. ThreadPoolExecutor 中的Worker是如何執行提交到線程池的任務的?多余Worker怎么在超出空閑時間后被干掉的?
44. ThreadPoolExecutor shutdown、shutdownNow內部核心流程
45. 再回頭看看為啥不推薦Executors提供幾種線程池?
46. ThreadPoolExecutor線程池篇總結
總結
以上是生活随笔為你收集整理的18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win7 不支持此接口问题
- 下一篇: 分类性能度量指标:ROC曲线、AUC值、