AtomicLong 是不是該淘汰了?

Photo By Instagram sooyaaa

問題 5.

相信你一定記得學習併發編程的一個入門級例子,多個線程操作一個變量,累加 10000 次,最後結果居然不是 10000。後來你把這個變量換成了併發包中的原子類型變量 AtomicLong,完美的解決了併發問題。假如面試官問:還有更好的選擇嗎?LongAdder 瞭解過嗎?你能對答如流嗎?

我的答案

AtomicLong 是 Java 1.5 併發包中提供的一個原子類,他提供給了我們在多線程環境下安全的併發操作一個整數的特性。並且性能還可以,它主要依賴了 2 個技術,volatile 關鍵字和 CAS 原子指令,不知道這倆個技術的小夥伴參考往期的文章:ReentranLock 實現原理居然是這樣?AtomicLong 性能已經不錯了,但是當在線程高度競爭的狀況下性能會急劇下降,因爲高度競爭下 CAS 操作會耗費大量的失敗計算,因爲當一個線程去更新變量時候發現值早已經被其他線程更新了。那麼有沒有更好的解決方案呢,於是 LongAdder 誕生了。

LongAdder 是 Java 1.8 併發包中提供的一個工具類,它採用了一個分散熱點數據的思路。簡單來說,Atomic 中所有線程都去更新內存中的一個變量,而 LongAdder 中會有一個 Cell 類型的數組,這個數組的長度是 CPU 的核心數,因爲一臺電腦中最多同時會有 CPU 核心數個線程並行運行,每個線程更新數據時候會被映射到一個 Cell 元素去更新,這樣就將原本一個熱點的數據,分散成了多個數據,降低了熱點,這樣也就減少了線程的競爭程度,同時也就提高了更新的效率。當然這樣也帶了一個問題,就是更新函數不會返回更新後的值,而 AtomicLong 的更新方法會返回更新後的結果,LongAdder 只有在調用 sum 方法的時候纔會去累加每個 Cell 中的數據,然後返回結果。當然 LongAdder 中也用到了volatile 和 CAS 原子操作,所以小夥伴們一定要掌握這倆個技術點,這是面試必問的點。

既然說 LongAdder 的效率更好,那我們就來一段測試代碼,小小展示一下 LongAdder 的膩害之處,請看如下:

public class AtomicLongTester {


    private static AtomicLong numA = new AtomicLong(0);
    private static LongAdder numB = new LongAdder();




    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i < 10001; i*=10) {
            test(false, i);
            test(true, i);
        }
    }




    public static void test(boolean isLongAdder, int threadCount) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        final CountDownLatch latch = new CountDownLatch(threadCount);


        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        if (isLongAdder) {
                            numB.add(1);
                        } else {
                            numA.addAndGet(1);
                        }
                    }


                    latch.countDown();
                }
            }).start();
        }


        // 等待所有運算結束
        latch.await();


        if (isLongAdder) {
            System.out.println("Thread Count=" + threadCount + ", LongAdder cost ms=" + (System.currentTimeMillis() - starTime) + ", Result=" + numB.sum());
        } else {
            System.out.println("Thread Count=" + threadCount + ", AtomicLong cost ms=" + (System.currentTimeMillis() - starTime) + ", Result=" + numA.get());
        }


        numA = new AtomicLong(0);
        numB = new LongAdder();
    }


}

實驗結果大致如下:

Thread Count=1, AtomicLong cost ms=9, Result=100000
Thread Count=1, LongAdder cost ms=13, Result=100000
Thread Count=10, AtomicLong cost ms=14, Result=1000000
Thread Count=10, LongAdder cost ms=41, Result=1000000
Thread Count=100, AtomicLong cost ms=111, Result=10000000
Thread Count=100, LongAdder cost ms=45, Result=10000000
Thread Count=1000, AtomicLong cost ms=1456, Result=100000000
Thread Count=1000, LongAdder cost ms=379, Result=100000000
Thread Count=10000, AtomicLong cost ms=17452, Result=1000000000
Thread Count=10000, LongAdder cost ms=3545, Result=1000000000

從上面的結果可以看出來,當線程競爭率比較低的時候 AtomicLong 效率還是優於 LongAdder 的,但是當線程競爭率增大的時候,我們可以看出來 LongAdder 的性能遠遠高於 AtomicLong。

因此在使用原子類的時候,我們要結合實際情況,如果競爭率很高,那麼建議使用 LongAdder 來替代 AtomicLong。說到 LongAdder 也不得的說一說 volatile 帶來僞共享問題,對僞共享感興趣的同學歡迎關注後續的文章,我們會在後續的文章中探討這個問題。

以上即爲昨天的問題的答案,小夥伴們對這個答案是否滿意呢?歡迎留言和我討論。

又要到年末了,你是不是又悄咪咪的開始看機會啦。爲了廣大小夥伴能充足電量,能順利通過 BAT 的面試官無情三連炮,我特意推出大型刷題節目。每天一道題目,第二天給答案,前一天給小夥伴們獨立思考的機會。

點下“在看”,鼓勵一下?

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