摘要:在并發(fā)量較低的環(huán)境下,線程沖突的概率比較小,自旋的次數(shù)不會(huì)很多。比如有三個(gè),每個(gè)線程對(duì)增加。的核心方法還是通過(guò)例子來(lái)看假設(shè)現(xiàn)在有一個(gè)對(duì)象,四個(gè)線程同時(shí)對(duì)進(jìn)行累加操作。
本文首發(fā)于一世流云的專欄:https://segmentfault.com/blog...一、LongAdder簡(jiǎn)介
JDK1.8時(shí),java.util.concurrent.atomic包中提供了一個(gè)新的原子類:LongAdder。
根據(jù)Oracle官方文檔的介紹,LongAdder在高并發(fā)的場(chǎng)景下會(huì)比它的前輩————AtomicLong 具有更好的性能,代價(jià)是消耗更多的內(nèi)存空間:
那么,問(wèn)題來(lái)了:
為什么要引入LongAdder? AtomicLong在高并發(fā)的場(chǎng)景下有什么問(wèn)題嗎? 如果低并發(fā)環(huán)境下,LongAdder和AtomicLong性能差不多,那LongAdder是否就可以替代AtomicLong了?為什么要引入LongAdder?
我們知道,AtomicLong是利用了底層的CAS操作來(lái)提供并發(fā)性的,比如addAndGet方法:
上述方法調(diào)用了Unsafe類的getAndAddLong方法,該方法是個(gè)native方法,它的邏輯是采用自旋的方式不斷更新目標(biāo)值,直到更新成功。
在并發(fā)量較低的環(huán)境下,線程沖突的概率比較小,自旋的次數(shù)不會(huì)很多。但是,高并發(fā)環(huán)境下,N個(gè)線程同時(shí)進(jìn)行自旋操作,會(huì)出現(xiàn)大量失敗并不斷自旋的情況,此時(shí)AtomicLong的自旋會(huì)成為瓶頸。
這就是LongAdder引入的初衷——解決高并發(fā)環(huán)境下AtomicLong的自旋瓶頸問(wèn)題。
LongAdder快在哪里?既然說(shuō)到LongAdder可以顯著提升高并發(fā)環(huán)境下的性能,那么它是如何做到的?這里先簡(jiǎn)單的說(shuō)下LongAdder的思路,第二部分會(huì)詳述LongAdder的原理。
我們知道,AtomicLong中有個(gè)內(nèi)部變量value保存著實(shí)際的long值,所有的操作都是針對(duì)該變量進(jìn)行。也就是說(shuō),高并發(fā)環(huán)境下,value變量其實(shí)是一個(gè)熱點(diǎn),也就是N個(gè)線程競(jìng)爭(zhēng)一個(gè)熱點(diǎn)。
LongAdder的基本思路就是分散熱點(diǎn),將value值分散到一個(gè)數(shù)組中,不同線程會(huì)命中到數(shù)組的不同槽中,各個(gè)線程只對(duì)自己槽中的那個(gè)值進(jìn)行CAS操作,這樣熱點(diǎn)就被分散了,沖突的概率就小很多。如果要獲取真正的long值,只要將各個(gè)槽中的變量值累加返回。
這種做法有沒(méi)有似曾相識(shí)的感覺(jué)?沒(méi)錯(cuò),ConcurrentHashMap中的“分段鎖”其實(shí)就是類似的思路。
LongAdder能否替代AtomicLong?回答這個(gè)問(wèn)題之前,我們先來(lái)看下LongAdder提供的API:
可以看到,LongAdder提供的API和AtomicLong比較接近,兩者都能以原子的方式對(duì)long型變量進(jìn)行增減。
但是AtomicLong提供的功能其實(shí)更豐富,尤其是addAndGet、decrementAndGet、compareAndSet這些方法。
addAndGet、decrementAndGet除了單純的做自增自減外,還可以立即獲取增減后的值,而LongAdder則需要做同步控制才能精確獲取增減后的值。如果業(yè)務(wù)需求需要精確的控制計(jì)數(shù),做計(jì)數(shù)比較,AtomicLong也更合適。
另外,從空間方面考慮,LongAdder其實(shí)是一種“空間換時(shí)間”的思想,從這一點(diǎn)來(lái)講AtomicLong更適合。當(dāng)然,如果你一定要跟我杠現(xiàn)代主機(jī)的內(nèi)存對(duì)于這點(diǎn)消耗根本不算什么,那我也辦法。
總之,低并發(fā)、一般的業(yè)務(wù)場(chǎng)景下AtomicLong是足夠了。如果并發(fā)量很多,存在大量寫(xiě)多讀少的情況,那LongAdder可能更合適。適合的才是最好的,如果真出現(xiàn)了需要考慮到底用AtomicLong好還是LongAdder的業(yè)務(wù)場(chǎng)景,那么這樣的討論是沒(méi)有意義的,因?yàn)檫@種情況下要么進(jìn)行性能測(cè)試,以準(zhǔn)確評(píng)估在當(dāng)前業(yè)務(wù)場(chǎng)景下兩者的性能,要么換個(gè)思路尋求其它解決方案。
最后,給出國(guó)外一位博主對(duì)LongAdder和AtomicLong的性能評(píng)測(cè),以供參考:http://blog.palominolabs.com/...
二、LongAdder原理之前說(shuō)了,AtomicLong是多個(gè)線程針對(duì)單個(gè)熱點(diǎn)值value進(jìn)行原子操作。而LongAdder是每個(gè)線程擁有自己的槽,各個(gè)線程一般只對(duì)自己槽中的那個(gè)值進(jìn)行CAS操作。
比如有三個(gè)ThreadA、ThreadB、ThreadC,每個(gè)線程對(duì)value增加10。
對(duì)于AtomicLong,最終結(jié)果的計(jì)算始終是下面這個(gè)形式:
$$ value = 10 + 10 + 10 = 30 $$
但是對(duì)于LongAdder來(lái)說(shuō),內(nèi)部有一個(gè)base變量,一個(gè)Cell[]數(shù)組。
base變量:非競(jìng)態(tài)條件下,直接累加到該變量上
Cell[]數(shù)組:競(jìng)態(tài)條件下,累加個(gè)各個(gè)線程自己的槽Cell[i]中
最終結(jié)果的計(jì)算是下面這個(gè)形式:
$$ value = base + sum_{i=0}^nCell[i] $$
LongAdder只有一個(gè)空構(gòu)造器,其本身也沒(méi)有什么特殊的地方,所有復(fù)雜的邏輯都在它的父類Striped64中。
來(lái)看下Striped64的內(nèi)部結(jié)構(gòu),這個(gè)類實(shí)現(xiàn)一些核心操作,處理64位數(shù)據(jù)。
Striped64只有一個(gè)空構(gòu)造器,初始化時(shí),通過(guò)Unsafe獲取到類字段的偏移量,以便后續(xù)CAS操作:
上面有個(gè)比較特殊的字段是threadLocalRandomProbe,可以把它看成是線程的hash值。這個(gè)后面我們會(huì)講到。
定義了一個(gè)內(nèi)部Cell類,這就是我們之前所說(shuō)的槽,每個(gè)Cell對(duì)象存有一個(gè)value值,可以通過(guò)Unsafe來(lái)CAS操作它的值:
其它的字段:
可以看到Cell[]就是之前提到的槽數(shù)組,base就是非并發(fā)條件下的基數(shù)累計(jì)值。
還是通過(guò)例子來(lái)看:
假設(shè)現(xiàn)在有一個(gè)LongAdder對(duì)象la,四個(gè)線程A、B、C、D同時(shí)對(duì)la進(jìn)行累加操作。
LongAdder la = new LongAdder(); la.add(10);
①ThreadA調(diào)用add方法(假設(shè)此時(shí)沒(méi)有并發(fā)):
初始時(shí)Cell[]為null,base為0。所以ThreadA會(huì)調(diào)用casBase方法(定義在Striped64中),因?yàn)闆](méi)有并發(fā),CAS操作成功將base變?yōu)?0:
可以看到,如果線程A、B、C、D線性執(zhí)行,那casBase永遠(yuǎn)不會(huì)失敗,也就永遠(yuǎn)不會(huì)進(jìn)入到base方法的if塊中,所有的值都會(huì)累積到base中。
那么,如果任意線程有并發(fā)沖突,導(dǎo)致caseBase失敗呢?
失敗就會(huì)進(jìn)入if方法體:
這個(gè)方法體會(huì)先再次判斷Cell[]槽數(shù)組有沒(méi)初始化過(guò),如果初始化過(guò)了,以后所有的CAS操作都只針對(duì)槽中的Cell;否則,進(jìn)入longAccumulate方法。
整個(gè)add方法的邏輯如下圖:
可以看到,只有從未出現(xiàn)過(guò)并發(fā)沖突的時(shí)候,base基數(shù)才會(huì)使用到,一旦出現(xiàn)了并發(fā)沖突,之后所有的操作都只針對(duì)Cell[]數(shù)組中的單元Cell。
如果Cell[]數(shù)組未初始化,會(huì)調(diào)用父類的longAccumelate去初始化Cell[],如果Cell[]已經(jīng)初始化但是沖突發(fā)生在Cell單元內(nèi),則也調(diào)用父類的longAccumelate,此時(shí)可能就需要對(duì)Cell[]擴(kuò)容了。
這也是LongAdder設(shè)計(jì)的精妙之處:盡量減少熱點(diǎn)沖突,不到最后萬(wàn)不得已,盡量將CAS操作延遲。
Striped64的核心方法我們來(lái)看下Striped64的核心方法longAccumulate到底做了什么:
上述代碼首先給當(dāng)前線程分配一個(gè)hash值,然后進(jìn)入一個(gè)自旋,這個(gè)自旋分為三個(gè)分支:
CASE1:Cell[]數(shù)組已經(jīng)初始化
CASE2:Cell[]數(shù)組未初始化
CASE3:Cell[]數(shù)組正在初始化中
CASE2:Cell[]數(shù)組未初始化我們之前討論了,初始時(shí)Cell[]數(shù)組還沒(méi)有初始化,所以會(huì)進(jìn)入分支②:
首先會(huì)將cellsBusy置為1-加鎖狀態(tài)
然后,初始化Cell[]數(shù)組(初始大小為2),根據(jù)當(dāng)前線程的hash值計(jì)算映射的索引,并創(chuàng)建對(duì)應(yīng)的Cell對(duì)象,Cell單元中的初始值x就是本次要累加的值。
CASE3:Cell[]數(shù)組正在初始化中如果在初始化過(guò)程中,另一個(gè)線程ThreadB也進(jìn)入了longAccumulate方法,就會(huì)進(jìn)入分支③:
可以看到,分支③直接操作base基數(shù),將值累加到base上。
CASE1:Cell[]數(shù)組已經(jīng)初始化如果初始化完成后,其它線程也進(jìn)入了longAccumulate方法,就會(huì)進(jìn)入分支①:
整個(gè)longAccumulate的流程圖如下:
最后,我們來(lái)看下LongAdder的sum方法:
sum求和的公式就是我們開(kāi)頭說(shuō)的:
$$ value = base + sum_{i=0}^nCell[i] $$
需要注意的是,這個(gè)方法只能得到某個(gè)時(shí)刻的近似值,這也就是LongAdder并不能完全替代LongAtomic的原因之一。
三、LongAdder的其它兄弟JDK1.8時(shí),java.util.concurrent.atomic包中,除了新引入LongAdder外,還有引入了它的三個(gè)兄弟類:LongAccumulator、DoubleAdder、DoubleAccumulator
LongAccumulatorLongAccumulator是LongAdder的增強(qiáng)版。LongAdder只能針對(duì)數(shù)值的進(jìn)行加減運(yùn)算,而LongAccumulator提供了自定義的函數(shù)操作。其構(gòu)造函數(shù)如下:
通過(guò)LongBinaryOperator,可以自定義對(duì)入?yún)⒌娜我獠僮鳎⒎祷亟Y(jié)果(LongBinaryOperator接收2個(gè)long作為參數(shù),并返回1個(gè)long)
LongAccumulator內(nèi)部原理和LongAdder幾乎完全一樣,都是利用了父類Striped64的longAccumulate方法。這里就不再贅述了,讀者可以自己閱讀源碼。
DoubleAdder和DoubleAccumulator從名字也可以看出,DoubleAdder和DoubleAccumulator用于操作double原始類型。
與LongAdder的唯一區(qū)別就是,其內(nèi)部會(huì)通過(guò)一些方法,將原始的double類型,轉(zhuǎn)換為long類型,其余和LongAdder完全一樣:
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/76607.html
摘要:整個(gè)包,按照功能可以大致劃分如下鎖框架原子類框架同步器框架集合框架執(zhí)行器框架本系列將按上述順序分析,分析所基于的源碼為。后,根據(jù)一系列常見(jiàn)的多線程設(shè)計(jì)模式,設(shè)計(jì)了并發(fā)包,其中包下提供了一系列基礎(chǔ)的鎖工具,用以對(duì)等進(jìn)行補(bǔ)充增強(qiáng)。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首發(fā)于一世流云專欄:https...
摘要:注意原子數(shù)組并不是說(shuō)可以讓線程以原子方式一次性地操作數(shù)組中所有元素的數(shù)組。類的方法返回指定類型數(shù)組的元素所占用的字節(jié)數(shù)。,是將轉(zhuǎn)換為進(jìn)制,然后從左往右數(shù)連續(xù)的個(gè)數(shù)。 showImg(https://segmentfault.com/img/remote/1460000016012145); 本文首發(fā)于一世流云的專欄:https://segmentfault.com/blog... 一...
摘要:本身不直接支持指針的操作,所以這也是該類命名為的原因之一。中的許多方法,內(nèi)部其實(shí)都是類在操作。 showImg(https://segmentfault.com/img/remote/1460000016012251); 本文首發(fā)于一世流云的專欄:https://segmentfault.com/blog... 一、Unsafe簡(jiǎn)介 在正式的開(kāi)講 juc-atomic框架系列之前,有...
摘要:所謂,就是可以以一種線程安全的方式操作非線程安全對(duì)象的某些字段。我們來(lái)對(duì)上述代碼進(jìn)行改造賬戶類改造引入通過(guò)操作字段調(diào)用方,并未做任何改變上述代碼,無(wú)論執(zhí)行多少次,最終結(jié)果都是,因?yàn)檫@回是線程安全的。這也是整個(gè)包的設(shè)計(jì)理念之一。 showImg(https://segmentfault.com/img/remote/1460000016012109); 本文首發(fā)于一世流云的專欄:http...
摘要:顧名思義,是類型的線程安全原子類,可以在應(yīng)用程序中以原子的方式更新值。創(chuàng)建對(duì)象先來(lái)看下對(duì)象的創(chuàng)建。也就是說(shuō)當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),其它線程能立即讀到這個(gè)修改的值。 showImg(https://segmentfault.com/img/remote/1460000016012210); 本文首發(fā)于一世流云的專欄:https://segmentfault.com/blog... ...
閱讀 971·2023-04-25 23:55
閱讀 2691·2023-04-25 14:13
閱讀 3286·2019-08-26 13:47
閱讀 2957·2019-08-23 18:16
閱讀 618·2019-08-23 17:20
閱讀 3217·2019-08-23 16:55
閱讀 3136·2019-08-22 15:39
閱讀 3185·2019-08-20 18:10