關於LongAdder的一點思考

LongAdder是jdk 1.8引入的一個類,宣稱比AtomiLong更高效。它內部有一個基本數base和一個cell數組,在高併發的情況下各個線程將值存放在了數組中,在低併發的情況下直接在一個base數上做計算,取值的時候把基本數和cell數組中的值做累加返回。LongAdder採用了類似分段鎖的設計,降低了競爭。

其實看到取值的時候我是懵的,取值的代碼如下:

 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;
    }

註釋裏說了,當沒有併發更新的時候,這個值是準確的。當有併發更新的時候,這個值就不準確了。原因很簡單,雖然base和cells都是volatile類型,保證了可見性,然而我累加到一半的時候,base因爲併發更新而改變了,可我已經累加過了,拿到的結果肯定沒有包括這期間新增的值。

令人沮喪的是,在併發期間,這個真實的值幾乎是不可用的。雖然LongAdder存起來了各個確定的值,但是我卻拿不到這個準確的和。

因此LongAdder,應該只適用於對計數不要求那麼精確(雖然它是精確的保存了所有值的),或者只關心最終的值(併發已經結束,可以拿到最終的結果)。在阿里巴巴JAVA開發手冊中,稍微提到了LongAdder在做類似count++操作方面的優勢,如果只是想統計下最後的個數,那麼LongAdder肯定是最好的選擇。
在這裏插入圖片描述
下面附上LongAdder和AtomicLong在做count++上面的比較,測試代碼如下:

@Data
@Slf4j
public class LongAdderTest{

    public static final int COR = 1000;

    public static final int MAX = 10000;

    public static final int THREAD_COUNT = 1000; // 通過修改併發線程數和循環次數來測試

    public static final int LOOP = 1000000; // 通過修改併發線程數和循環次數來測試

    public static void main(String[] args) throws Exception{
        ExecutorService pool = new ThreadPoolExecutor(COR,MAX,60L, TimeUnit.SECONDS,new LinkedBlockingQueue<>());

        final LongAdder adder = new LongAdder();
        final AtomicLong count = new AtomicLong();
        CompletionService completionService = new ExecutorCompletionService<>(pool);
        long start = System.currentTimeMillis();
        for(int i =0;i<THREAD_COUNT;i++){
            completionService.submit(() -> {
                for (int j = 0; j < LOOP; j++) {
                    //count.incrementAndGet();
                    adder.increment();
                }
                return 1L;
            });
        }

        for (int i = 0; i < THREAD_COUNT; i++) {
            Future<Long> future = completionService.take();
            future.get();
        }
        System.out.println("耗時:" + (System.currentTimeMillis() - start) + "ms");

        pool.shutdown();
    }

我們固定循環100W次,然後改變併發線程數,下面是測試結果:

10 100 1000
AtomicLong 441ms 2.931s 28.851s
LongAdder 203ms 706ms 6.526s

可以看到,在低併發的時候兩者區別並不是很大,在高併發的時候兩者耗時就不是一個數量級了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章