面試官問:AtomicInteger 在高并發(fā)下性能不好,為什么?
- AtomicLong 存在的問題
- LongAdder 帶來的改進(jìn)和原理
- 如何選擇
- AtomicLong 可否被 LongAdder 替代?
- 結(jié)論
最近秋招陸陸續(xù)續(xù)開始了,讀者跟我反饋一個(gè)面試真題,關(guān)于AtomicInteger 在高并發(fā)下的性能問題。上一篇我們已經(jīng)提及atomic家族
原子類Atomic家族,一家人就是要整整齊齊
我們知道在 JDK1.5 中新增了并發(fā)情況下使用的 Integer/Long 所對(duì)應(yīng)的原子類 AtomicInteger 和 AtomicLong。
在并發(fā)的場景下,如果我們需要實(shí)現(xiàn)計(jì)數(shù)器,可以利用 AtomicInteger 和 AtomicLong,這樣一來,就可以避免加鎖和復(fù)雜的代碼邏輯,有了它們之后,我們只需要執(zhí)行對(duì)應(yīng)的封裝好的方法,例如對(duì)這兩個(gè)變量進(jìn)行原子的增操作或原子的減操作,就可以滿足大部分業(yè)務(wù)場景的需求。
不過,雖然它們很好用,但是如果你的業(yè)務(wù)場景是并發(fā)量很大的,那么你也會(huì)發(fā)現(xiàn),這兩個(gè)原子類實(shí)際上會(huì)有較大的性能問題,就讓我們從一個(gè)例子看起。
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ù)。
那我們往下看這個(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ā)場景,因?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 而言,它才能感知到這次變化。
由于競爭很激烈,這樣的 flush 和 refresh 操作耗費(fèi)了很多資源,而且 CAS 也會(huì)經(jīng)常失敗。
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 是用在競爭不激烈的情況下的,可以直接把累加結(jié)果改到 base 變量上。
那么,當(dāng)競爭激烈的時(shí)候,就要用到我們的 Cell[] 數(shù)組了。一旦競爭激烈,各個(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 的思想類似。
競爭激烈的時(shí)候,LongAdder 會(huì)通過計(jì)算出每個(gè)線程的 hash 值來給線程分配到不同的 Cell 上去,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來就不會(huì)和其他的計(jì)數(shù)器干擾,Cell 之間并不存在競爭關(guān)系,所以在自加的過程中,就大大減少了剛才的 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í)候并沒有進(jìn)行加鎖操作,所以這里得出的 sum 不一定是完全準(zhǔn)確的,因?yàn)橛锌赡茉谟?jì)算 sum 的過程中 Cell 的值被修改了。
如何選擇
在低競爭的情況下,AtomicLong 和 LongAdder 這兩個(gè)類具有相似的特征,吞吐量也是相似的,因?yàn)楦偁幉桓摺5窃诟偁幖ち业那闆r下,LongAdder 的預(yù)期吞吐量要高得多,經(jīng)過試驗(yàn),LongAdder 的吞吐量大約是 AtomicLong 的十倍,不過凡事總要付出代價(jià),LongAdder 在保證高效的同時(shí),也需要消耗更多的空間。
AtomicLong 可否被 LongAdder 替代?
那么我們就要考慮了,有了更高效的 LongAdder,那 AtomicLong 可否不使用了呢?是否凡是用到 AtomicLong 的地方,都可以用 LongAdder 替換掉呢?
答案是不是的,這需要區(qū)分場景。
LongAdder 只提供了 add、increment 等簡單的方法,適合的是統(tǒng)計(jì)求和計(jì)數(shù)的場景,場景比較單一,而 AtomicLong 還具有 compareAndSet 等高級(jí)方法,可以應(yīng)對(duì)除了加減之外的更復(fù)雜的需要 CAS 的場景。
結(jié)論
如果我們的場景僅僅是需要用到加和減操作的話,那么可以直接使用更高效的 LongAdder,但如果我們需要利用 CAS 比如 compareAndSet 等操作的話,就需要使用 AtomicLong 來完成。