AtomicXXX 用的好好的,阿里為什么推薦使用 LongAdder?面試必問(wèn)!
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)
面試連環(huán)炮
先來(lái)一連炮簡(jiǎn)單的面試,看你能頂住幾輪?
棧長(zhǎng):
1、多線(xiàn)程情況下,進(jìn)行數(shù)字累加(count++)要注意什么?
張三:
要注意給累加方法加同步鎖,不然會(huì)出現(xiàn)變量可見(jiàn)性問(wèn)題,變量值被其他線(xiàn)程覆蓋出現(xiàn)不一致的情況
棧長(zhǎng):
2、保證變量可見(jiàn)性,用 volatile 修飾不就行了嗎?
張三:
volatile 是可以保證可見(jiàn)性,但不能保證原子性和線(xiàn)程安全
棧長(zhǎng):
3、除了加同步鎖這種方案,還有別的方法嗎?
張三:
還可以用 JDK 中的原子類(lèi),比如:AtomicInteger、AtomicLong,它們是通過(guò) CAS 算法實(shí)現(xiàn)的一種樂(lè)觀鎖
棧長(zhǎng):
4、不錯(cuò),還知道別的么?
張三:
呃……
認(rèn)真的,你能頂住幾輪?
這些問(wèn)題是 Java 程序員面試過(guò)程中必問(wèn)的,出場(chǎng)率賊高,Java 程序員必懂,這些題在Java面試庫(kù)小程序中也都有詳細(xì)答案,這里就不展開(kāi)了。
你還知道別的么?最后一輪的答案就是今天的主題!
更好的選擇:LongAdder
你還在用 AtomicInteger、AtomicLong 原子類(lèi)進(jìn)行并發(fā)累加操作嗎?那你就 OUT 了!
除了 AtomicInteger、AtomicLong,其實(shí)在 JDK 8 中更建議使用 LongAdder 進(jìn)行原子性操作,性能更好,如果你使用的還是 JDK 7-,那當(dāng)我沒(méi)說(shuō),即使如此,也不能找借口不知道,畢竟 JDK 8 是現(xiàn)在的主流應(yīng)用版本了。
阿里巴巴最新的 Java開(kāi)發(fā)手冊(cè) 是這么定義的:

這份阿里巴巴完整的 Java 開(kāi)發(fā)手冊(cè),可以關(guān)注公眾號(hào):Java核心技術(shù),回復(fù):手冊(cè),即可下載高清完整版。
如果你還沒(méi)有用過(guò) LongAdder,不妨看看本文,刷新你的認(rèn)知,棧長(zhǎng)帶你漲知識(shí)!
為什么搞出了 LongAdder?
我們都知道在 JDK 5 中搞出了 AtomicInteger、AtomicLong 等原子類(lèi),這也是在 JDK 8 之前普遍用的原子性操作類(lèi),來(lái)看下 AtomicLong 的累加源碼:

大家都知道這些原子類(lèi)都是通過(guò) CAS 算法實(shí)現(xiàn)的樂(lè)觀鎖,通過(guò)舊值和現(xiàn)有的值不斷循環(huán)比對(duì),直到比對(duì)成功才修改成功結(jié)束循環(huán)。
這樣就會(huì)有一個(gè)問(wèn)題,如果并發(fā)數(shù)很高的話(huà),就會(huì)造成過(guò)多的沒(méi)有必要的 "循環(huán)",這勢(shì)必會(huì)影響 CPU 的性能。
所以,JDK 8 又搞出來(lái)了一個(gè) LongAdder,也在 atomic 包下:

大家可以看到,在同級(jí)包中還有一個(gè) LongAccumulator 類(lèi),這個(gè)這篇不展開(kāi),棧長(zhǎng)下次再另開(kāi)一篇具體分析,關(guān)注公眾號(hào):Java技術(shù)棧,寫(xiě)完我會(huì)第一時(shí)間進(jìn)行推送。
LongAdder 為什么性能更好?
來(lái)分析下 LongAdder 類(lèi)的源碼:

在 LongAdder 中維護(hù)了一個(gè) Cell 數(shù)組,當(dāng) Cell 它不為空時(shí),size 是 2 的次冪大小,每個(gè) Cell 數(shù)組里面都有一個(gè)初始值為 0 的 long 變量,用來(lái)存儲(chǔ)每個(gè) Cell 的值:

然后其中的 sum 方法用來(lái)對(duì) Cell 數(shù)組進(jìn)行求和再加上 base 基礎(chǔ)值進(jìn)行返回:

關(guān)于 base 基礎(chǔ)值:
LongAdder 并不會(huì)一開(kāi)始就創(chuàng)建 Cell 數(shù)組,其本身也會(huì)維護(hù)一個(gè) base 基礎(chǔ)值,當(dāng) CAS 更新失敗時(shí)才進(jìn)行創(chuàng)建或者擴(kuò)容。
來(lái)看下 AtomicXXX 和 LongAdder 更新對(duì)比圖:

Cell 數(shù)組相當(dāng)于一個(gè)分段的概念,把 AtomicXXX 中的一個(gè)值分成了多個(gè)值進(jìn)行管理,當(dāng) CAS 更新失敗時(shí)不再當(dāng)前循環(huán)重試,而是嘗試獲取其他的資源鎖,這樣就降低了對(duì)于 AtomicXXX 中的單個(gè)資源的競(jìng)爭(zhēng),所以 LongAdder 的性能更高。
雖然 LongAdder 性能更好,那有沒(méi)有缺點(diǎn)呢?
LongAdder 帶來(lái)了良好的性能,代價(jià)肯定也是有的,既然維護(hù)了 Cell 數(shù)組,也就意味著要占用更多的內(nèi)存空間,以空間換時(shí)間,也是值得的。
實(shí)戰(zhàn)測(cè)試
既然官方都說(shuō)在高并發(fā)的情況下性能更好,事實(shí)是否如此呢?
棧長(zhǎng)必須實(shí)戰(zhàn)測(cè)試一翻,打消大家的疑慮!
AtomicLong 測(cè)試代碼:
/**
?*?@author:?棧長(zhǎng)
?*?@from:?公眾號(hào)Java技術(shù)棧
?*/
private?static?void?atomicLongTest()?throws?InterruptedException?{
????long?start?=?System.currentTimeMillis();
????ExecutorService?es?=?Executors.newFixedThreadPool(MAX_POOL_SIZE);
????for?(int?i?=?0;?i?????????es.execute(()?->?{
????????????for?(int?j?=?0;?j?????????????????atomicLong.incrementAndGet();
????????????}
????????});
????}
????es.shutdown();
????es.awaitTermination(5,?TimeUnit.MINUTES);
????System.out.printf("AtomicLong %s*%s 結(jié)果:%s,耗時(shí):%sms.\n",
????????????MAX_POOL_SIZE,
????????????MAX_LOOP_SIZE,
????????????atomicLong.get(),
????????????(System.currentTimeMillis()?-?start));
}
LongAdder 測(cè)試代碼:
/**
?*?@author:?棧長(zhǎng)
?*?@from:?公眾號(hào)Java技術(shù)棧
?*/
private?static?void?longAdderTest()?throws?InterruptedException?{
????long?start?=?System.currentTimeMillis();
????ExecutorService?es?=?Executors.newFixedThreadPool(MAX_POOL_SIZE);
????for?(int?i?=?0;?i?????????es.execute(()?->?{
????????????for?(int?j?=?0;?j?????????????????longAdder.increment();
????????????}
????????});
????}
????es.shutdown();
????es.awaitTermination(5,?TimeUnit.MINUTES);
????System.out.printf("LongAdder %s*%s 結(jié)果:%s,耗時(shí):%sms.\n",
????????????MAX_POOL_SIZE,
????????????MAX_LOOP_SIZE,
????????????longAdder.sum(),
????????????(System.currentTimeMillis()?-?start));
}
這里只貼核心測(cè)試代碼了,完整代碼已上傳到了 Github:
https://github.com/javastacks/javastack
測(cè)試結(jié)果:

這里測(cè)試的只有是 1 個(gè)線(xiàn)程,每個(gè)線(xiàn)程循環(huán)累加 1 次,這個(gè)沒(méi)有鎖競(jìng)爭(zhēng)、沒(méi)有高并發(fā)操作的場(chǎng)景就能看出性能上的差異了。。
棧長(zhǎng)再不斷提升 線(xiàn)程數(shù)、循環(huán)累加次數(shù) ,得到了以下測(cè)試結(jié)果:
| 線(xiàn)程數(shù) * 循環(huán)次數(shù) | AtomicLong | LongAdder |
|---|---|---|
| 1 * 1 | 45ms | 1ms |
| 10 * 10 | 55ms | 2ms |
| 10 * 100 | 56ms | 2ms |
| 100 * 10 | 58ms | 10ms |
| 100 * 100 | 74ms | 10ms |
| 1000 * 10 | 190ms | 71ms |
| 1000 * 100 | 217ms | 73ms |
| 1000 * 1000 | 194ms | 81ms |
| 1000 * 10000 | 301ms | 114ms |
| 1000 * 100000 | 1813ms | 277ms |
| 1000 * 1000000 | 17596ms | 1629ms |
圖表對(duì)比:

從測(cè)試結(jié)果可以看出,LongAdder 的性能都是碾壓 AtomicLong 的,最高可達(dá) 28 多倍的差距(56/2),可以說(shuō)在高性能要求的高并發(fā)場(chǎng)景,肯定是有必要用 LongAdder 的,這也是阿里巴巴為什么建議使用 LongAdder 的原因。
當(dāng)然,這只是我個(gè)人的測(cè)試,這個(gè)也和硬件配置有關(guān)系的,但毋庸置疑是,AtomicLong 的性能是更好的。另外,棧長(zhǎng)寫(xiě)的 Java 多線(xiàn)程系列教程全部整理好了,微信搜索Java技術(shù)棧,在后臺(tái)發(fā)送:Java,可以在線(xiàn)閱讀。
總結(jié)
本文以一場(chǎng)面試連環(huán)炮揭開(kāi)了 LongAdder 的面紗,怎么解決 count++ 的線(xiàn)程安全性問(wèn)題?
棧長(zhǎng)再總結(jié)下:
累加方法加 synchronized/ Lock 同步鎖; 使用 AtomicInteger/ AtomicLong 原子類(lèi); 使用 LongAdder 原子類(lèi)(推薦使用);
LongAdder 這個(gè)東西是 Java 8 搞出來(lái)的,用來(lái)代替 AtomicXXX,不管是否高并發(fā)場(chǎng)景,都完勝 AtomicXXX,它不僅可以改善性能,現(xiàn)在面試也問(wèn)的越來(lái)越多了,大家還是有必要掌握。
雖然 LongAdder 性能更好,但也是以更多的內(nèi)存空間消耗為代價(jià)的,當(dāng)然,現(xiàn)代計(jì)算機(jī),內(nèi)存早已不是瓶頸,所以這點(diǎn)消耗是可以忽略不計(jì)的,性能還是最重要的,但是大家也要知道這個(gè)點(diǎn)。
本文實(shí)戰(zhàn)源代碼完整版已經(jīng)上傳:
https://github.com/javastacks/javastack
歡迎 Star 學(xué)習(xí),公眾號(hào)所有 Java 實(shí)戰(zhàn)示例都會(huì)在這上面提供!
好了,今天的分享就到這里了,后面棧長(zhǎng)會(huì)分享更多好玩的 Java 技術(shù)和最新的技術(shù)資訊,關(guān)注公眾號(hào)Java技術(shù)棧第一時(shí)間推送,我也將主流 Java 面試題和參考答案都整理好了,在公眾號(hào)后臺(tái)回復(fù)關(guān)鍵字 "面試" 進(jìn)行刷題。
最后,覺(jué)得我的文章對(duì)你用收獲的話(huà),動(dòng)動(dòng)小手,給個(gè)在看、轉(zhuǎn)發(fā),原創(chuàng)不易,棧長(zhǎng)需要你的鼓勵(lì)。
版權(quán)聲明: 本文系公眾號(hào) "Java技術(shù)棧" 原創(chuàng),轉(zhuǎn)載、引用本文內(nèi)容請(qǐng)注明出處,抄襲、洗稿一律投訴侵權(quán),后果自負(fù),并保留追究其法律責(zé)任的權(quán)利。

關(guān)注Java技術(shù)棧看更多干貨


