如果是count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
首先我們來個例子:
publicclassAtomicMain{publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServiceservice=Executors.newCachedThreadPool();Countcount=newCount();//100個線程對共享變量進行加1for(inti=0;i<100;i++){service.execute(()->count.increase());}//等待上述的線程執行完service.shutdown();service.awaitTermination(1,TimeUnit.DAYS);System.out.println("公眾號:Java3y---------");System.out.println(count.getCount());}}classCount{//共享變量privateIntegercount=0;publicIntegergetCount(){returncount;}publicvoidincrease(){count++;}}
你們猜猜得出的結果是多少?是100嗎?
多運行幾次可以發現:結果是不確定的,可能是95,也可能是98,也可能是100
根據結果我們得知:上面的代碼是線程不安全的!如果線程安全的代碼,多次執行的結果是一致的!
我們可以發現問題所在:count++
并不是原子操作。因為count++
需要經過讀取-修改-寫入
三個步驟。舉個例子:
如果某一個時刻:線程A讀到count的值是10,線程B讀到count的值也是10
線程A對count++
,此時count的值為11
線程B對count++
,此時count的值也是11(因為線程B讀到的count是10)
所以到這里應該知道為啥我們的結果是不確定了吧。
要將上面的代碼變成線程安全的(每次得出的結果是100),那也很簡單,畢竟我們是學過synchronized鎖的人:
在increase()
加synchronized鎖就好了
publicsynchronizedvoidincrease(){count++;}
無論執行多少次,得出的都是100:
從上面的代碼我們也可以發現,只做一個++
這么簡單的操作,都用到了synchronized鎖,未免有點小題大做了。
Synchronized鎖是獨占的,意味著如果有別的線程在執行,當前線程只能是等待!
于是我們原子變量的類就登場了!
1.2CAS再來看看
在寫文章之前,本以為對CAS有一定的了解了(因為之前已經看過相關概念,以為自己理解了)..但真正敲起鍵盤寫的時候,還是發現沒完全弄懂...所以再來看看CAS吧。
來源維基百科:
比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程編程中實現不被打斷的數據交換操作,從而避免多線程同時改寫某一數據時由于執行順序不確定性以及中斷的不可預知性產生的數據不一致問題。 該操作通過將內存中的值與指定數據進行比較,當數值一樣時將內存中的數據替換為新的值。
CAS有3個操作數:
內存值V
舊的預期值A
要修改的新值B
當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內存值V相同時,將內存值V修改為B),而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試(或者什么都不做)。
我們畫張圖來理解一下:
我們可以發現CAS有兩種情況:
如果內存值V和我們的預期值A相等,則將內存值修改為B,操作成功!
如果內存值V和我們的預期值A不相等,一般也有兩種情況:
重試(自旋)
什么都不做
我們再繼續往下看,如果內存值V和我們的預期值A不相等時,應該什么時候重試,什么時候什么都不做。
1.2.1CAS失敗重試(自旋)
比如說,我上面用了100個線程,對count值進行加1。我們都知道:如果在線程安全的情況下,這個count值最終的結果一定是為100的。那就意味著:每個線程都會對這個count值實質地進行加1。
我繼續畫張圖來說明一下CAS是如何重試(循環再試)的:
上面圖只模擬出兩個線程的情況,但足夠說明問題了。
1.2.2CAS失敗什么都不做
上面是每個線程都要為count值加1,但我們也可以有這種情況:將count值設置為5
我也來畫個圖說明一下:
理解CAS的核心就是:CAS是原子性的,雖然你可能看到比較后再修改(compare and swap)覺得會有兩個操作,但終究是原子性的!
原子變量類在java.util.concurrent.atomic
包下,總體來看有這么多個:
我們可以對其進行分類:
基本類型:
AtomicBoolean:布爾型
AtomicInteger:整型
AtomicLong:長整型
數組:
AtomicIntegerArray:數組里的整型
AtomicLongArray:數組里的長整型
AtomicReferenceArray:數組里的引用類型
引用類型:
AtomicReference:引用類型
AtomicStampedReference:帶有版本號的引用類型
AtomicMarkableReference:帶有標記位的引用類型
對象的屬性:
AtomicIntegerFieldUpdater:對象的屬性是整型
AtomicLongFieldUpdater:對象的屬性是長整型
AtomicReferenceFieldUpdater:對象的屬性是引用類型
JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
是對AtomicLong等類的改進。比如LongAccumulator與LongAdder在高并發環境下比AtomicLong更高效。
Atomic包里的類基本都是使用Unsafe實現的包裝類。
Unsafe里邊有幾個我們喜歡的方法(CAS):
//第一和第二個參數代表對象的實例以及地址,第三個參數代表期望值,第四個參數代表更新值publicfinalnativebooleancompareAndSwapObject(Objectvar1,longvar2,Objectvar4,Objectvar5);publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);publicfinalnativebooleancompareAndSwapLong(Objectvar1,longvar2,longvar4,longvar6);
從原理上概述就是:Atomic包的類的實現絕大調用Unsafe的方法,而Unsafe底層實際上是調用C代碼,C代碼調用匯編,最后生成出一條CPU指令cmpxchg,完成操作。這也就為啥CAS是原子性的,因為它是一條CPU指令,不會被打斷。
2.1原子變量類使用
既然我們上面也說到了,使用Synchronized鎖有點小題大作了,我們用原子變量類來改一下:
classCount{//共享變量(使用AtomicInteger來替代Synchronized鎖)privateAtomicIntegercount=newAtomicInteger(0);publicIntegergetCount(){returncount.get();}publicvoidincrease(){count.incrementAndGet();}}//Main方法還是如上
修改完,無論執行多少次,我們的結果永遠是100!
其實Atomic包下原子類的使用方式都不會差太多,了解原子類各種類型,看看API,基本就會用了(網上也寫得比較詳細,所以我這里果斷偷懶了)...
2.2ABA問題
使用CAS有個缺點就是ABA的問題,什么是ABA問題呢?首先我用文字描述一下:
現在我有一個變量count=10
,現在有三個線程,分別為A、B、C
線程A和線程C同時讀到count變量,所以線程A和線程C的內存值和預期值都為10
此時線程A使用CAS將count值修改成100
修改完后,就在這時,線程B進來了,讀取得到count的值為100(內存值和預期值都是100),將count值修改成10
線程C拿到執行權,發現內存值是10,預期值也是10,將count值修改成11
上面的操作都可以正常執行完的,這樣會發生什么問題呢??線程C無法得知線程A和線程B修改過的count值,這樣是有風險的。
下面我再畫個圖來說明一下ABA的問題(以鏈表為例):
2.3解決ABA問題
要解決ABA的問題,我們可以使用JDK給我們提供的AtomicStampedReference和AtomicMarkableReference類。
AtomicStampedReference:
An {@code AtomicStampedReference} maintains an object referencealong with an integer "stamp", that can be updated atomically.
簡單來說就是在給為這個對象提供了一個版本,并且這個版本如果被修改了,是自動更新的。
原理大概就是:維護了一個Pair對象,Pair對象存儲我們的對象引用和一個stamp值。每次CAS比較的是兩個Pair對象
//Pair對象privatestaticclassPair<T>{finalTreference;finalintstamp;privatePair(Treference,intstamp){this.reference=reference;this.stamp=stamp;}static<T>Pair<T>of(Treference,intstamp){returnnewPair<T>(reference,stamp);}}privatevolatilePair<V>pair;//比較的是Pari對象publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp){Pair<V>current=pair;returnexpectedReference==current.reference&&expectedStamp==current.stamp&&((newReference==current.reference&&newStamp==current.stamp)||casPair(current,Pair.of(newReference,newStamp)));}
因為多了一個版本號比較,所以就不會存在ABA的問題了。
2.4LongAdder性能比AtomicLong要好
如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
去查閱了一些博客和資料,大概的意思就是:
使用AtomicLong時,在高并發下大量線程會同時去競爭更新同一個原子變量,但是由于同時只有一個線程的CAS會成功,所以其他線程會不斷嘗試自旋嘗試CAS操作,這會浪費不少的CPU資源。
而LongAdder可以概括成這樣:內部核心數據value分離成一個數組(Cell),每個線程訪問時,通過哈希等算法映射到其中一個數字進行計數,而最終的計數結果,則為這個數組的求和累加。
簡單來說就是將一個值分散成多個值,在并發的時候就可以分散壓力,性能有所提高。
看完了這篇文章,相信你對“Atomic有什么用”有了一定的了解,如果想了解更多相關知識,歡迎關注本站行業資訊頻道,感謝各位的閱讀!
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
c語言中正確的字符常量是用一對單引號將一個字符括起表示合法的字符常量。例如‘a’。數值包括整型、浮點型。整型可用十進制,八進制,十六進制。八進制前面要加0,后面...
2022年天津專場考試原定于3月19日舉行,受疫情影響確定延期,但目前延期后的考試時間推遲。 符合報名條件的考生,須在規定時間登錄招考資訊網(www.zha...
:喜歡聽,樂意看。指很受歡迎?!巴卣官Y料”喜聞樂見:[ xǐ wén lè jiàn ]詳細解釋1. 【解釋】:喜歡聽,樂意看。指很受歡迎。2. 【示例】:這是...
如今,喝酒已經成為我們生活中不可或缺的交流方式,但如果只是在酒桌上喝酒聊天,時間久了,就會無話可說,甚至尷尬。那么游戲無疑成為了打破壁壘的最佳途徑。無論是剛認識的人快速熱身,還是聊什么都在玩的朋友,只要游戲一上來,氣氛立馬活躍起來。所以酒桌上一定要有幾個小游戲,讓你在酒桌上大放異彩。邊肖總結了幾個你可以學習的常見游戲。以后會派上用場的。01.十五點二十游戲數量:游戲規則:兩個人面對面伸出拳頭。拳頭...
G20峰會是一個國際經濟合作論壇,于1999年12月16日在德國柏林成立,屬于布雷頓森林體系框架內非正式對話的一種機制,由原八國集團以及其余十二個重要經濟體組成。G20金融峰會旨在推動已工業化的發達國家和新興市場國家之間就實質性問題進行開放及有建設性的討論和研究,以尋求合作并促進國際金融穩定和經濟的持續增長。按照以往慣例,國際貨幣基金組織與世界銀行列席該組織的會議。G20叫做20國集團,由八國集團...
特斯拉概念股2023年有:(資料圖片僅供參考)申達股份:截止15點收盤,申達股份報3.420元,漲1.48%,總市值37.89億元。申達股份近7個交易日,期間整體上漲2.05%,最高價為3.32元,最低價為3.43元,總成交量3332.61萬手。2023年來下跌-6.14%。申達股份從近五年ROTA來看,近五年ROTA均值為-1.43%,過去五年ROTA最低為2020年的-9.72%,最高為201...