面試官問(wèn):AtomicInteger 在高并發(fā)下性能不好,為什么?
- AtomicLong 存在的問(wèn)題
 - LongAdder 帶來(lái)的改進(jìn)和原理
 - 如何選擇
 - AtomicLong 可否被 LongAdder 替代?
 - 結(jié)論
 
最近秋招陸陸續(xù)續(xù)開始了,讀者跟我反饋一個(gè)面試真題,關(guān)于AtomicInteger 在高并發(fā)下的性能問(wèn)題。上一篇我們已經(jīng)提及atomic家族
原子類Atomic家族,一家人就是要整整齊齊
我們知道在 JDK1.5 中新增了并發(fā)情況下使用的 Integer/Long 所對(duì)應(yīng)的原子類 AtomicInteger 和 AtomicLong。
在并發(fā)的場(chǎng)景下,如果我們需要實(shí)現(xiàn)計(jì)數(shù)器,可以利用 AtomicInteger 和 AtomicLong,這樣一來(lái),就可以避免加鎖和復(fù)雜的代碼邏輯,有了它們之后,我們只需要執(zhí)行對(duì)應(yīng)的封裝好的方法,例如對(duì)這兩個(gè)變量進(jìn)行原子的增操作或原子的減操作,就可以滿足大部分業(yè)務(wù)場(chǎng)景的需求。
不過(guò),雖然它們很好用,但是如果你的業(yè)務(wù)場(chǎng)景是并發(fā)量很大的,那么你也會(huì)發(fā)現(xiàn),這兩個(gè)原子類實(shí)際上會(huì)有較大的性能問(wèn)題,就讓我們從一個(gè)例子看起。
AtomicLong 存在的問(wèn)題
首先我們來(lái)看一段代碼:
- /**
 - * 描述: 在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ù)。
那我們往下看這個(gè)任務(wù)是什么。在下面的 Task 類中可以看到,這個(gè)任務(wù)實(shí)際上就是每一次去調(diào)用 AtomicLong 的 incrementAndGet 方法,相當(dāng)于一次自加操作。這樣一來(lái),整個(gè)類的作用就是把這個(gè)原子類從 0 開始,添加 100 個(gè)任務(wù),每個(gè)任務(wù)自加一次。
這段代碼的運(yùn)行結(jié)果毫無(wú)疑問(wèn)是 100,雖然是多線程并發(fā)訪問(wèn),但是 AtomicLong 依然可以保證 incrementAndGet 操作的原子性,所以不會(huì)發(fā)生線程安全問(wèn)題。
不過(guò)如果我們深入一步去看內(nèi)部情景的話,你可能會(huì)感到意外。我們把模型簡(jiǎn)化成只有兩個(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 修飾的,所以它需要保證自身可見性。
這樣一來(lái),每一次它的數(shù)值有變化的時(shí)候,它都需要進(jìn)行 flush 和 refresh。比如說(shuō),如果開始時(shí),ctr 的數(shù)值為 0 的話,那么如圖所示,一旦 core 1 把它改成 1 的話,它首先會(huì)在左側(cè)把這個(gè) 1 的最新結(jié)果給 flush 到下方的共享內(nèi)存。然后,再到右側(cè)去往上 refresh 到核心 2 的本地內(nèi)存。這樣一來(lái),對(duì)于核心 2 而言,它才能感知到這次變化。
由于競(jìng)爭(zhēng)很激烈,這樣的 flush 和 refresh 操作耗費(fèi)了很多資源,而且 CAS 也會(huì)經(jīng)常失敗。
LongAdder 帶來(lái)的改進(jìn)和原理
在 JDK 8 中又新增了 LongAdder 這個(gè)類,這是一個(gè)針對(duì) Long 類型的操作工具類。那么既然已經(jīng)有了 AtomicLong,為何又要新增 LongAdder 這么一個(gè)類呢?
我們同樣是用一個(gè)例子來(lái)說(shuō)明。下面這個(gè)例子和剛才的例子很相似,只不過(guò)我們把工具類從 AtomicLong 變成了 LongAdder。其他的不同之處還在于最終打印結(jié)果的時(shí)候,調(diào)用的方法從原來(lái)的 get 變成了現(xiàn)在的 sum 方法。而其他的邏輯都一樣。
我們來(lái)看一下使用 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è)。
這樣一來(lái),LongAdder 會(huì)把不同線程對(duì)應(yīng)到不同的 Cell 上進(jìn)行修改,降低了沖突的概率,這是一種分段的理念,提高了并發(fā)性,這就和 Java 7 的 ConcurrentHashMap 的 16 個(gè) Segment 的思想類似。
競(jìng)爭(zhēng)激烈的時(shí)候,LongAdder 會(huì)通過(guò)計(jì)算出每個(gè)線程的 hash 值來(lái)給線程分配到不同的 Cell 上去,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來(lái)就不會(huì)和其他的計(jì)數(shù)器干擾,Cell 之間并不存在競(jìng)爭(zhēng)關(guān)系,所以在自加的過(guò)程中,就大大減少了剛才的 flush 和 refresh,以及降低了沖突的概率,因?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í)候并沒(méi)有進(jìn)行加鎖操作,所以這里得出的 sum 不一定是完全準(zhǔn)確的,因?yàn)橛锌赡茉谟?jì)算 sum 的過(guò)程中 Cell 的值被修改了。
如何選擇
在低競(jìng)爭(zhēng)的情況下,AtomicLong 和 LongAdder 這兩個(gè)類具有相似的特征,吞吐量也是相似的,因?yàn)楦?jìng)爭(zhēng)不高。但是在競(jìng)爭(zhēng)激烈的情況下,LongAdder 的預(yù)期吞吐量要高得多,經(jīng)過(guò)試驗(yàn),LongAdder 的吞吐量大約是 AtomicLong 的十倍,不過(guò)凡事總要付出代價(jià),LongAdder 在保證高效的同時(shí),也需要消耗更多的空間。
AtomicLong 可否被 LongAdder 替代?
那么我們就要考慮了,有了更高效的 LongAdder,那 AtomicLong 可否不使用了呢?是否凡是用到 AtomicLong 的地方,都可以用 LongAdder 替換掉呢?
答案是不是的,這需要區(qū)分場(chǎng)景。
LongAdder 只提供了 add、increment 等簡(jiǎn)單的方法,適合的是統(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 來(lái)完成。
















 
 
 














 
 
 
 