四十一唉擂、原子類(二)高并發(fā)下的AtomicInteger

subject:AtomicInteger 在高并發(fā)下性能不好,如何解決檀葛?為什么玩祟?

在 JDK1.5 中新增了并發(fā)情況下使用的 Integer/Long 所對(duì)應(yīng)的原子類 AtomicInteger 和 AtomicLong。

在并發(fā)的場(chǎng)景下屿聋,如果需要實(shí)現(xiàn)計(jì)數(shù)器空扎,可以利用 AtomicInteger 和 AtomicLong,這樣一來润讥,就可以避免加鎖和復(fù)雜的代碼邏輯转锈,有了它們之后,我們只需要執(zhí)行對(duì)應(yīng)的封裝好的方法楚殿,例如對(duì)這兩個(gè)變量進(jìn)行原子的增操作或原子的減操作撮慨,就可以滿足大部分業(yè)務(wù)場(chǎng)景的需求。

不過脆粥,雖然它們很好用砌溺,但是如果業(yè)務(wù)場(chǎng)景是并發(fā)量很大的,那么這兩個(gè)原子類實(shí)際上會(huì)有較大的性能問題变隔,這是為什么呢抚吠?

1、AtomicLong 存在的問題

首先來看一段代碼:

/**
* 描述:     在16個(gè)線程下使用AtomicLong
*/
public class AtomicLongDemo {
 
   public static void main(String[] args) throws InterruptedException {
       AtomicLong counter = new AtomicLong(0);
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.get());
   }
 
   static class Task implements Runnable {
 
       private final AtomicLong counter;
 
       public Task(AtomicLong counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.incrementAndGet();
       }
   }
}

在這段代碼中可以看出弟胀,新建了一個(gè)原始值為 0 的 AtomicLong。然后,有一個(gè)線程數(shù)為 16 的線程池孵户,并且往這個(gè)線程池中添加了 100 次相同的一個(gè)任務(wù)萧朝。

在下面的 Task 類中可以看到,這個(gè)任務(wù)實(shí)際上就是每一次去調(diào)用 AtomicLong 的 incrementAndGet 方法夏哭,相當(dāng)于一次自加操作检柬。這樣一來,整個(gè)類的作用就是把這個(gè)原子類從 0 開始竖配,添加 100 個(gè)任務(wù)何址,每個(gè)任務(wù)自加一次。

這段代碼的運(yùn)行結(jié)果毫無疑問是 100进胯,雖然是多線程并發(fā)訪問用爪,但是 AtomicLong 依然可以保證 incrementAndGet 操作的原子性,所以不會(huì)發(fā)生線程安全問題胁镐。

不過如果深入一步去看內(nèi)部情景的話偎血,可能會(huì)感到意外。把模型簡化成只有兩個(gè)線程在同時(shí)工作的并發(fā)場(chǎng)景盯漂,因?yàn)閮蓚€(gè)線程和更多個(gè)線程本質(zhì)上是一樣的颇玷。如圖所示:

在這個(gè)圖中,每一個(gè)線程是運(yùn)行在自己的 core 中的就缆,并且它們都有一個(gè)本地內(nèi)存是自己獨(dú)用的帖渠。在本地內(nèi)存下方,有兩個(gè) CPU 核心共用的共享內(nèi)存竭宰。

對(duì)于 AtomicLong 內(nèi)部的 value 屬性而言空郊,也就是保存當(dāng)前 AtomicLong 數(shù)值的屬性,它是被 volatile 修飾的羞延,所以它需要保證自身可見性渣淳。

這樣一來,每一次它的數(shù)值有變化的時(shí)候伴箩,它都需要進(jìn)行 flush 和 refresh入愧。比如說,如果開始時(shí)嗤谚,ctr 的數(shù)值為 0 的話棺蛛,那么如圖所示,一旦 core 1 把它改成 1 的話巩步,它首先會(huì)在左側(cè)把這個(gè) 1 的最新結(jié)果給 flush 到下方的共享內(nèi)存旁赊。然后,再到右側(cè)去往上 refresh 到核心 2 的本地內(nèi)存椅野。這樣一來终畅,對(duì)于核心 2 而言籍胯,它才能感知到這次變化。

由于競(jìng)爭(zhēng)很激烈离福,這樣的 flush 和 refresh 操作耗費(fèi)了很多資源杖狼,而且 CAS 也會(huì)經(jīng)常失敗。

2妖爷、LongAdder 帶來的改進(jìn)和原理

在 JDK 8 中又新增了 LongAdder 這個(gè)類蝶涩,這是一個(gè)針對(duì) Long 類型的操作工具類。既然已經(jīng)有了 AtomicLong絮识,為何又要新增 LongAdder 這么一個(gè)類呢绿聘?

同樣是用一個(gè)例子來說明。下面這個(gè)例子和剛才的例子很相似次舌,只不過把工具類從 AtomicLong 變成了 LongAdder熄攘。其他的不同之處還在于最終打印結(jié)果的時(shí)候,調(diào)用的方法從原來的 get 變成了現(xiàn)在的 sum 方法垃它。其他的邏輯都一樣鲜屏。

來看一下使用 LongAdder 的代碼示例:

/**
* 描述:     在16個(gè)線程下使用LongAdder
*/
public class LongAdderDemo {
 
   public static void main(String[] args) throws InterruptedException {
       LongAdder counter = new LongAdder();
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.sum());
   }
   static class Task implements Runnable {
 
       private final LongAdder counter;
 
       public Task(LongAdder counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.increment();
       }
   }
}

代碼的運(yùn)行結(jié)果同樣是 100,但是運(yùn)行速度比剛才 AtomicLong 的實(shí)現(xiàn)要快国拇。下面解釋一下洛史,為什么高并發(fā)下 LongAdder 比 AtomicLong 效率更高。

因?yàn)?LongAdder 引入了分段累加的概念酱吝,內(nèi)部一共有兩個(gè)參數(shù)參與計(jì)數(shù):第一個(gè)叫作 base也殖,它是一個(gè)變量,第二個(gè)是 Cell[] 务热,是一個(gè)數(shù)組忆嗜。

其中的 base 是用在競(jìng)爭(zhēng)不激烈的情況下的,可以直接把累加結(jié)果改到 base 變量上崎岂。

那么捆毫,當(dāng)競(jìng)爭(zhēng)激烈的時(shí)候,就要用到 Cell[] 數(shù)組了冲甘。一旦競(jìng)爭(zhēng)激烈绩卤,各個(gè)線程會(huì)分散累加到自己所對(duì)應(yīng)的那個(gè) Cell[] 數(shù)組的某一個(gè)對(duì)象中,而不會(huì)大家共用同一個(gè)江醇。

這樣一來濒憋,LongAdder 會(huì)把不同線程對(duì)應(yīng)到不同的 Cell 上進(jìn)行修改,降低了沖突的概率陶夜,這是一種分段的理念凛驮,提高了并發(fā)性,這就和 Java 7 的 ConcurrentHashMap 的 16 個(gè) Segment 的思想類似条辟。

競(jìng)爭(zhēng)激烈的時(shí)候黔夭,LongAdder 會(huì)通過計(jì)算出每個(gè)線程的 hash 值來給線程分配到不同的 Cell 上去宏胯,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來就不會(huì)和其他的計(jì)數(shù)器干擾本姥,Cell 之間并不存在競(jìng)爭(zhēng)關(guān)系胳嘲,所以在自加的過程中,就大大減少了剛才的 flush 和 refresh扣草,以及降低了沖突的概率,這就是為什么 LongAdder 的吞吐量比 AtomicLong 大的原因颜屠,本質(zhì)是空間換時(shí)間辰妙,因?yàn)樗卸鄠€(gè)計(jì)數(shù)器同時(shí)在工作,所以占用的內(nèi)存也要相對(duì)更大一些甫窟。

那么 LongAdder 最終是如何實(shí)現(xiàn)多線程計(jì)數(shù)的呢密浑?答案就在最后一步的求和 sum 方法,執(zhí)行 LongAdder.sum() 的時(shí)候粗井,會(huì)把各個(gè)線程里的 Cell 累計(jì)求和尔破,并加上 base,形成最終的總和浇衬。代碼如下:

public long sum() {
   Cell[] as = cells; Cell a;
   long sum = base;
   if (as != null) {
       for (int i = 0; i < as.length; ++i) {
           if ((a = as[i]) != null)
               sum += a.value;
       }
   }
   return sum;
}

在這個(gè) sum 方法中可以看到懒构,思路非常清晰。先取 base 的值耘擂,然后遍歷所有 Cell胆剧,把每個(gè) Cell 的值都加上去,形成最終的總和醉冤。由于在統(tǒng)計(jì)的時(shí)候并沒有進(jìn)行加鎖操作秩霍,所以這里得出的 sum 不一定是完全準(zhǔn)確的,因?yàn)橛锌赡茉谟?jì)算 sum 的過程中 Cell 的值被修改了蚁阳。

3铃绒、如何選擇?AtomicLong 可否被 LongAdder 替代螺捐?

現(xiàn)在已經(jīng)了解了颠悬,為什么 AtomicLong 或者說 AtomicInteger 它在高并發(fā)下性能不好,也同時(shí)看到了性能更好的 LongAdder归粉。下面就分析一下椿疗,對(duì)它們應(yīng)該如何選擇。

在低競(jìng)爭(zhēng)的情況下糠悼,AtomicLong 和 LongAdder 這兩個(gè)類具有相似的特征,吞吐量也是相似的倔喂,因?yàn)楦?jìng)爭(zhēng)不高铝条。但是在競(jìng)爭(zhēng)激烈的情況下,LongAdder 的預(yù)期吞吐量要高得多班缰,經(jīng)過試驗(yàn),LongAdder 的吞吐量大約是 AtomicLong 的十倍埠忘,不過凡事總要付出代價(jià)脾拆,LongAdder 在保證高效的同時(shí)莹妒,也需要消耗更多的空間。

LongAdder 只提供了 add旨怠、increment 等簡單的方法,適合的是統(tǒng)計(jì)求和計(jì)數(shù)的場(chǎng)景鉴腻,場(chǎng)景比較單一迷扇,而 AtomicLong 還具有 compareAndSet 等高級(jí)方法,可以應(yīng)對(duì)除了加減之外的更復(fù)雜的需要 CAS 的場(chǎng)景爽哎。

結(jié)論:如果場(chǎng)景僅僅是需要用到加和減操作的話蜓席,那么可以直接使用更高效的 LongAdder,但如果需要利用 CAS 比如 compareAndSet 等操作的話倦青,就需要使用 AtomicLong 來完成瓮床。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市产镐,隨后出現(xiàn)的幾起案子隘庄,更是在濱河造成了極大的恐慌,老刑警劉巖癣亚,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑掺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡述雾,警方通過查閱死者的電腦和手機(jī)街州,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玻孟,“玉大人唆缴,你說我怎么就攤上這事∈螋幔” “怎么了面徽?”我有些...
    開封第一講書人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我趟紊,道長氮双,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任霎匈,我火速辦了婚禮戴差,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铛嘱。我一直安慰自己暖释,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開白布墨吓。 她就那樣靜靜地躺著饭入,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肛真。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評(píng)論 1 314
  • 那天爽航,我揣著相機(jī)與錄音蚓让,去河邊找鬼。 笑死讥珍,一個(gè)胖子當(dāng)著我的面吹牛历极,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衷佃,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼趟卸,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了氏义?” 一聲冷哼從身側(cè)響起锄列,我...
    開封第一講書人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惯悠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體克婶,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡情萤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年筋岛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歇万。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贪磺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劫映,我是刑警寧澤刹前,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布喇喉,位于F島的核電站,受9級(jí)特大地震影響拣技,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜徐绑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一傲茄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盘榨,春花似錦蟆融、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至枪孩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蔑舞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來泰國打工从撼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留低零,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓掏婶,卻偏偏與公主長得像雄妥,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茎芭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361