摘要:失敗重試自旋比如說(shuō),我上面用了個(gè)線程,對(duì)值進(jìn)行加。我們都知道如果在線程安全的情況下,這個(gè)值最終的結(jié)果一定是為的。那就意味著每個(gè)線程都會(huì)對(duì)這個(gè)值實(shí)質(zhì)地進(jìn)行加。
前言
只有光頭才能變強(qiáng)
之前已經(jīng)寫(xiě)過(guò)多線程相關(guān)的文章了,有興趣的同學(xué)可以去了解一下:
https://github.com/ZhongFuCheng3y/3y/blob/master/src/thread.md
在閱讀《阿里巴巴 Java開(kāi)發(fā)手冊(cè)》讀后感時(shí),還有未解決的問(wèn)題:
如果是count++操作,使用如下類(lèi)實(shí)現(xiàn): AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推薦使用 LongAdder 對(duì)象,比 AtomicLong 性能更好(減少樂(lè)觀鎖的重試次數(shù))。
之前在學(xué)習(xí)的時(shí)候也看過(guò)AtomicInteger類(lèi)很多次了,一直沒(méi)有去做相關(guān)的筆記?,F(xiàn)在遇到問(wèn)題了,于是就過(guò)來(lái)寫(xiě)寫(xiě)筆記,并希望在學(xué)習(xí)的過(guò)程中解決掉問(wèn)題。
一、基礎(chǔ)鋪墊首先我們來(lái)個(gè)例子:
public class AtomicMain { public static void main(String[] args) throws InterruptedException { ExecutorService service = Executors.newCachedThreadPool(); Count count = new Count(); // 100個(gè)線程對(duì)共享變量進(jìn)行加1 for (int i = 0; i < 100; i++) { service.execute(() -> count.increase()); } // 等待上述的線程執(zhí)行完 service.shutdown(); service.awaitTermination(1, TimeUnit.DAYS); System.out.println("公眾號(hào):Java3y---------"); System.out.println(count.getCount()); } } class Count{ // 共享變量 private Integer count = 0; public Integer getCount() { return count; } public void increase() { count++; } }
你們猜猜得出的結(jié)果是多少?是100嗎?
多運(yùn)行幾次可以發(fā)現(xiàn):結(jié)果是不確定的,可能是95,也可能是98,也可能是100
根據(jù)結(jié)果我們得知:上面的代碼是線程不安全的!如果線程安全的代碼,多次執(zhí)行的結(jié)果是一致的!
我們可以發(fā)現(xiàn)問(wèn)題所在:count++并不是原子操作。因?yàn)?b>count++需要經(jīng)過(guò)讀取-修改-寫(xiě)入三個(gè)步驟。舉個(gè)例子:
如果某一個(gè)時(shí)刻:線程A讀到count的值是10,線程B讀到count的值也是10
線程A對(duì)count++,此時(shí)count的值為11
線程B對(duì)count++,此時(shí)count的值也是11(因?yàn)榫€程B讀到的count是10)
所以到這里應(yīng)該知道為啥我們的結(jié)果是不確定了吧。
要將上面的代碼變成線程安全的(每次得出的結(jié)果是100),那也很簡(jiǎn)單,畢竟我們是學(xué)過(guò)synchronized鎖的人:
在increase()加synchronized鎖就好了
public synchronized void increase() { count++; }
無(wú)論執(zhí)行多少次,得出的都是100:
從上面的代碼我們也可以發(fā)現(xiàn),只做一個(gè)++這么簡(jiǎn)單的操作,都用到了synchronized鎖,未免有點(diǎn)小題大做了。
Synchronized鎖是獨(dú)占的,意味著如果有別的線程在執(zhí)行,當(dāng)前線程只能是等待!
于是我們原子變量的類(lèi)就登場(chǎng)了!
1.2CAS再來(lái)看看在寫(xiě)文章之前,本以為對(duì)CAS有一定的了解了(因?yàn)橹耙呀?jīng)看過(guò)相關(guān)概念,以為自己理解了)..但真正敲起鍵盤(pán)寫(xiě)的時(shí)候,還是發(fā)現(xiàn)沒(méi)完全弄懂...所以再來(lái)看看CAS吧。
來(lái)源維基百科:
比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程編程中實(shí)現(xiàn)不被打斷的數(shù)據(jù)交換操作,從而避免多線程同時(shí)改寫(xiě)某一數(shù)據(jù)時(shí)由于執(zhí)行順序不確定性以及中斷的不可預(yù)知性產(chǎn)生的數(shù)據(jù)不一致問(wèn)題。 該操作通過(guò)將內(nèi)存中的值與指定數(shù)據(jù)進(jìn)行比較,當(dāng)數(shù)值一樣時(shí)將內(nèi)存中的數(shù)據(jù)替換為新的值。
CAS有3個(gè)操作數(shù):
內(nèi)存值V
舊的預(yù)期值A(chǔ)
要修改的新值B
當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能更新變量的值(A和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B),而其它線程都失敗,失敗的線程并不會(huì)被掛起,而是被告知這次競(jìng)爭(zhēng)中失敗,并可以再次嘗試(或者什么都不做)。
我們畫(huà)張圖來(lái)理解一下:
我們可以發(fā)現(xiàn)CAS有兩種情況:
如果內(nèi)存值V和我們的預(yù)期值A(chǔ)相等,則將內(nèi)存值修改為B,操作成功!
如果內(nèi)存值V和我們的預(yù)期值A(chǔ)不相等,一般也有兩種情況:
重試(自旋)
什么都不做
我們?cè)倮^續(xù)往下看,如果內(nèi)存值V和我們的預(yù)期值A(chǔ)不相等時(shí),應(yīng)該什么時(shí)候重試,什么時(shí)候什么都不做。
1.2.1CAS失敗重試(自旋)比如說(shuō),我上面用了100個(gè)線程,對(duì)count值進(jìn)行加1。我們都知道:如果在線程安全的情況下,這個(gè)count值最終的結(jié)果一定是為100的。那就意味著:每個(gè)線程都會(huì)對(duì)這個(gè)count值實(shí)質(zhì)地進(jìn)行加1。
我繼續(xù)畫(huà)張圖來(lái)說(shuō)明一下CAS是如何重試(循環(huán)再試)的:
上面圖只模擬出兩個(gè)線程的情況,但足夠說(shuō)明問(wèn)題了。
1.2.2CAS失敗什么都不做上面是每個(gè)線程都要為count值加1,但我們也可以有這種情況:將count值設(shè)置為5
我也來(lái)畫(huà)個(gè)圖說(shuō)明一下:
理解CAS的核心就是:CAS是原子性的,雖然你可能看到比較后再修改(compare and swap)覺(jué)得會(huì)有兩個(gè)操作,但終究是原子性的!
二、原子變量類(lèi)簡(jiǎn)單介紹原子變量類(lèi)在java.util.concurrent.atomic包下,總體來(lái)看有這么多個(gè):
我們可以對(duì)其進(jìn)行分類(lèi):
基本類(lèi)型:
AtomicBoolean:布爾型
AtomicInteger:整型
AtomicLong:長(zhǎng)整型
數(shù)組:
AtomicIntegerArray:數(shù)組里的整型
AtomicLongArray:數(shù)組里的長(zhǎng)整型
AtomicReferenceArray:數(shù)組里的引用類(lèi)型
引用類(lèi)型:
AtomicReference:引用類(lèi)型
AtomicStampedReference:帶有版本號(hào)的引用類(lèi)型
AtomicMarkableReference:帶有標(biāo)記位的引用類(lèi)型
對(duì)象的屬性:
AtomicIntegerFieldUpdater:對(duì)象的屬性是整型
AtomicLongFieldUpdater:對(duì)象的屬性是長(zhǎng)整型
AtomicReferenceFieldUpdater:對(duì)象的屬性是引用類(lèi)型
JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
是對(duì)AtomicLong等類(lèi)的改進(jìn)。比如LongAccumulator與LongAdder在高并發(fā)環(huán)境下比AtomicLong更高效。
Atomic包里的類(lèi)基本都是使用Unsafe實(shí)現(xiàn)的包裝類(lèi)。
Unsafe里邊有幾個(gè)我們喜歡的方法(CAS):
// 第一和第二個(gè)參數(shù)代表對(duì)象的實(shí)例以及地址,第三個(gè)參數(shù)代表期望值,第四個(gè)參數(shù)代表更新值 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
從原理上概述就是:Atomic包的類(lèi)的實(shí)現(xiàn)絕大調(diào)用Unsafe的方法,而Unsafe底層實(shí)際上是調(diào)用C代碼,C代碼調(diào)用匯編,最后生成出一條CPU指令cmpxchg,完成操作。這也就為啥CAS是原子性的,因?yàn)樗且粭lCPU指令,不會(huì)被打斷。
2.1原子變量類(lèi)使用既然我們上面也說(shuō)到了,使用Synchronized鎖有點(diǎn)小題大作了,我們用原子變量類(lèi)來(lái)改一下:
class Count{ // 共享變量(使用AtomicInteger來(lái)替代Synchronized鎖) private AtomicInteger count = new AtomicInteger(0); public Integer getCount() { return count.get(); } public void increase() { count.incrementAndGet(); } } // Main方法還是如上
修改完,無(wú)論執(zhí)行多少次,我們的結(jié)果永遠(yuǎn)是100!
其實(shí)Atomic包下原子類(lèi)的使用方式都不會(huì)差太多,了解原子類(lèi)各種類(lèi)型,看看API,基本就會(huì)用了(網(wǎng)上也寫(xiě)得比較詳細(xì),所以我這里果斷偷懶了)...
2.2ABA問(wèn)題使用CAS有個(gè)缺點(diǎn)就是ABA的問(wèn)題,什么是ABA問(wèn)題呢?首先我用文字描述一下:
現(xiàn)在我有一個(gè)變量count=10,現(xiàn)在有三個(gè)線程,分別為A、B、C
線程A和線程C同時(shí)讀到count變量,所以線程A和線程C的內(nèi)存值和預(yù)期值都為10
此時(shí)線程A使用CAS將count值修改成100
修改完后,就在這時(shí),線程B進(jìn)來(lái)了,讀取得到count的值為100(內(nèi)存值和預(yù)期值都是100),將count值修改成10
線程C拿到執(zhí)行權(quán),發(fā)現(xiàn)內(nèi)存值是10,預(yù)期值也是10,將count值修改成11
上面的操作都可以正常執(zhí)行完的,這樣會(huì)發(fā)生什么問(wèn)題呢??線程C無(wú)法得知線程A和線程B修改過(guò)的count值,這樣是有風(fēng)險(xiǎn)的。
下面我再畫(huà)個(gè)圖來(lái)說(shuō)明一下ABA的問(wèn)題(以鏈表為例):
2.3解決ABA問(wèn)題要解決ABA的問(wèn)題,我們可以使用JDK給我們提供的AtomicStampedReference和AtomicMarkableReference類(lèi)。
AtomicStampedReference:
An {@code AtomicStampedReference} maintains an object referencealong with an integer "stamp", that can be updated atomically.
簡(jiǎn)單來(lái)說(shuō)就是在給為這個(gè)對(duì)象提供了一個(gè)版本,并且這個(gè)版本如果被修改了,是自動(dòng)更新的。
原理大概就是:維護(hù)了一個(gè)Pair對(duì)象,Pair對(duì)象存儲(chǔ)我們的對(duì)象引用和一個(gè)stamp值。每次CAS比較的是兩個(gè)Pair對(duì)象
// Pair對(duì)象 private static class Pair{ final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static Pair of(T reference, int stamp) { return new Pair (reference, stamp); } } private volatile Pair pair; // 比較的是Pari對(duì)象 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
因?yàn)槎嗔艘粋€(gè)版本號(hào)比較,所以就不會(huì)存在ABA的問(wèn)題了。
2.4LongAdder性能比AtomicLong要好如果是 JDK8,推薦使用 LongAdder 對(duì)象,比 AtomicLong 性能更好(減少樂(lè)觀鎖的重試次數(shù))。
去查閱了一些博客和資料,大概的意思就是:
使用AtomicLong時(shí),在高并發(fā)下大量線程會(huì)同時(shí)去競(jìng)爭(zhēng)更新同一個(gè)原子變量,但是由于同時(shí)只有一個(gè)線程的CAS會(huì)成功,所以其他線程會(huì)不斷嘗試自旋嘗試CAS操作,這會(huì)浪費(fèi)不少的CPU資源。
而LongAdder可以概括成這樣:內(nèi)部核心數(shù)據(jù)value分離成一個(gè)數(shù)組(Cell),每個(gè)線程訪問(wèn)時(shí),通過(guò)哈希等算法映射到其中一個(gè)數(shù)字進(jìn)行計(jì)數(shù),而最終的計(jì)數(shù)結(jié)果,則為這個(gè)數(shù)組的求和累加。
簡(jiǎn)單來(lái)說(shuō)就是將一個(gè)值分散成多個(gè)值,在并發(fā)的時(shí)候就可以分散壓力,性能有所提高。
參考資料:
AtomicLong與LongAdder性能對(duì)比https://zhuanlan.zhihu.com/p/45489739
LongAdder源碼詳解https://zhuanlan.zhihu.com/p/38288416
最后參考資料:
Java并發(fā)編程札記-目錄https://blog.csdn.net/panweiwei1994/article/details/78483167
《Java并發(fā)編程實(shí)戰(zhàn)》
《Java并發(fā)編程的藝術(shù)》
如果你覺(jué)得我寫(xiě)得還不錯(cuò),了解一下:
堅(jiān)持原創(chuàng)的技術(shù)公眾號(hào):Java3y?;貜?fù) 1 加入Java交流群
文章的目錄導(dǎo)航(精美腦圖+海量視頻資源):https://github.com/ZhongFuCheng3y/3y
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/72323.html
摘要:線程的這種交叉操作會(huì)導(dǎo)致線程不安全。原子操作是在多線程環(huán)境下避免數(shù)據(jù)不一致必須的手段。如果聲明一個(gè)域?yàn)橐恍┣闆r就可以確保多線程訪問(wèn)到的變量是最新的。并發(fā)要求一個(gè)線程對(duì)對(duì)象進(jìn)行了操作,對(duì)象發(fā)生了變化,這種變化應(yīng)該對(duì)其他線程是可見(jiàn)的。 雖是讀書(shū)筆記,但是如轉(zhuǎn)載請(qǐng)注明出處 http://segmentfault.com/blog/exploring/ .. 拒絕伸手復(fù)制黨 一個(gè)問(wèn)題: ...
摘要:今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線程面試題,希望對(duì)大家學(xué)習(xí)和面試都能有所幫助。指令重排在單線程環(huán)境下不會(huì)出先問(wèn)題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒(méi)有初始化的實(shí)例。使用可以禁止的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。 下面最近發(fā)的一些并發(fā)編程的文章匯總,通過(guò)閱讀這些文章大家再看大廠面試中的并發(fā)編程問(wèn)題就沒(méi)有那么頭疼了。今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線...
摘要:只有首先獲得鎖的任務(wù)線程才能繼續(xù)獲取該對(duì)象上的多個(gè)鎖。會(huì)進(jìn)一步對(duì)時(shí)失敗的那些線程進(jìn)行阻塞操作調(diào)用操作系統(tǒng)的信號(hào)量此段來(lái)摘自別處。提供了多樣化的同步,比如有時(shí)間限制的同步,可以被的同步的同步是不能的等。 各種方法 1?synchronized方法。 public synchronized void save(){} 2?synchronized代碼塊。 synchronized(obj...
摘要:架構(gòu)師入門(mén)筆記一初識(shí)線程關(guān)鍵字本章節(jié)主要介紹線程的關(guān)鍵字,的含義,使用方法,使用場(chǎng)景,以及注意事項(xiàng)。若次方法也加上了,就必須等待線程執(zhí)行完后,才能調(diào)用關(guān)鍵字不具備關(guān)鍵字的原子性同步其主要作用就是使變量在多個(gè)線程中可見(jiàn)。 架構(gòu)師入門(mén)筆記一 初識(shí)線程關(guān)鍵字 本章節(jié)主要介紹線程的關(guān)鍵字 synchronized,volatile 的含義,使用方法,使用場(chǎng)景,以及注意事項(xiàng)。 線程安全 首先我們...
閱讀 663·2021-09-24 09:48
閱讀 2499·2021-08-26 14:14
閱讀 526·2019-08-30 13:08
閱讀 1455·2019-08-29 15:22
閱讀 3086·2019-08-29 11:06
閱讀 1012·2019-08-26 18:26
閱讀 1070·2019-08-26 13:53
閱讀 2546·2019-08-26 12:21