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