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