原子操作增強類LongAdder

一. 性能對比

阿里開發手冊推薦jdk8使用LongAdder替代AtomicLong

 

 

 

 

示例代碼
題目:熱點商品點贊計算器,點贊數加加統計,不要求實時精確。50個線程,每個線程100W次,統計總點贊數

比較synchronized、AtomicInteger、AtomicLong、LongAdder、LongAccumulator五種計數性能

class ClickNumber{
    int number = 0;
    public synchronized void add_synchronized(){
        number++;
    }
 
    AtomicInteger atomicInteger = new AtomicInteger();
    public void add_AtomicInteger(){
        atomicInteger.incrementAndGet();
    }
 
    AtomicLong atomicLong = new AtomicLong();
    public void add_AtomicLong(){
        atomicLong.incrementAndGet();
    }
 
    LongAdder longAdder = new LongAdder();
    public void add_LongAdder(){
        longAdder.increment();
    }
 
    LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
    public void add_longAccumulator(){
        longAccumulator.accumulate(1);
    }
}
 
public class LongAdderCalcDemo {
    public static final int SIZE_THREAD = 50;
    public static final int _1w = 10000;
 
    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime = System.currentTimeMillis();
        long endTime = System.currentTimeMillis();
        CountDownLatch latch_synchronized = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_AtomicInteger= new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_AtomicLong = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_LongAdder = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_LongAccumulator = new CountDownLatch(SIZE_THREAD);
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_synchronized();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_synchronized.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_synchronized.await();
        endTime = System.currentTimeMillis();
        System.out.println("synchronized花費時間:"+ (endTime-startTime)+" 數值爲:"+clickNumber.number);
 
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_AtomicInteger();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_AtomicInteger.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_AtomicInteger.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicInteger花費時間:"+ (endTime-startTime)+" 數值爲:"+clickNumber.atomicInteger.get());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_AtomicLong();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_AtomicLong.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_AtomicLong.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicLong花費時間:"+ (endTime-startTime)+" 數值爲:"+clickNumber.atomicLong.get());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_LongAdder();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_LongAdder.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_LongAdder.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAdder花費時間:"+ (endTime-startTime)+" 數值爲:"+clickNumber.longAdder.longValue());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_longAccumulator();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_LongAccumulator.countDown();
                }
 
            },String.valueOf(i)).start();
        }
        latch_LongAccumulator.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAccumulator花費時間:"+ (endTime-startTime)+" 數值爲:"+clickNumber.longAccumulator.longValue());
 
    }
}

 

 

 


通過結果,可知LongAdder性能最優,花費時間最短,遠優於AtomicLong

二. LongAdder爲何那麼快

LongAdder的基本思路

LongAdder的基本思路就是分散熱點,將value值分散到一個Cell數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。sum()會將所有Cell數組中的value和base累加作爲返回值,

核心的思想就是將之前AtomicLong一個value的更新壓力分散到多個value中去,從而降級更新熱點。

LongAdder的原理


LongAdder在無競爭的情況,跟AtomicLong一樣,對同一個base進行操作(無併發,單線程下直接CAS操作更新base值;非競態條件下,直接累加到變量base上)

當出現競爭關係時則是採用化整爲零的做法,從空間換時間,用一個數組cells,將一個value拆分進這個數組Cells.多個線程需要同時對value進行操作時,可以對線程id進行hash得到hash值,再根據hash值映射到這個數組cells的某個下標,再對該下標所對應的值進行自增操作。當所有線程操作完畢,將數組cells的所有值和無競爭值base都加起來作爲最終結果。(有併發,多線程下分段CAS操作更新Cell數組值;競態條件下,累加個各個線程自己的槽Cell[]中)

 

 

 

 

sum()公式

 

 

 

三. 源碼解析

1. LongAdder.add()

 

 

 

Cell[] as; long b, v; int m; Cell a;

as是striped64中的cells數組屬性
b是striped64 中的base屬性 v是當前線程hash到的Cell中存儲的值
m是cells的長度減1,hash時作爲掩碼使用
a是當前線程hash到的Cell

if ((as = cells) != null || !casBase(b = base, b + x))

條件1: cells不爲空,說明出現過競爭,Ccell[]已創建
條件2: cas操作base失敗,說明其它線程先一步修改 了base正在出現競爭

首次首線程((as = cells) != null)一定是false,此時走casBase方法,以CAS的方式更新base值,且只有當cas失敗時,纔會走到if中

boolean uncontended = true

true無競爭,false表示競爭激烈,多個線程hash到同一個cell,可能要擴容

if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))

條件1: cells爲空,說明正在出現競爭,上面是從條件2過來的
條件2: 應該不會出現
條件3: 當前線程所在的cell爲空,說明當前線程還沒有更新過cell,應初始化一個cell
條件4: 更新當前線程所在的cell失敗,說明現在競爭很激烈,多個線程hash到了同一個個Cell,應擴容

longAccumulate(x, null, uncontended)

調用striped64中的方法處理

小結

  • 1.最初無競爭時只更新base;
  • 2.如果更新base失敗後,首次新建一個Cell[]數組
  • 3.當多個線程競爭同一個CelI比較激烈時,可能就要對Cell[]擴容

2. Striped64.longAccumulate()

longAccumulate()方法的入參

long X需要增加的值,一般默認都是1
LongBinaryOperator fn默認傳遞的是null
wasUncontended競爭標識,如果是false則代表有競爭。只有cells初始化之後, 並且當前
失敗,纔會是false

 

 

 

 

代碼解釋

上述代碼首先給當前線程分配一個hash值,然後進入一個for(;;)自旋,這個自旋分爲三個分支:
CASE1: Cell[]數組已經初始化

  


CASE2: Cell[]數組未初始化(首次新建)

 


如果上面條件都執行成功就會執行數組的初始化及賦值操作

Cell[] rs = new Cell[2]表示數組的長度爲2;rs[h & 1] = new Cell(x) 表示創建一個新的Cell元素,value是x值,默認爲1;h & 1類似於HashMap常用到的計算散列桶index的算法,通常都是hash & (table.len - 1),同hashmap一個意思

CASE3: Cell[]數組正在初始化中

 

  

多個線程嘗試CAS修改失敗的線程會走到這個分支,該分支實現直接操作base基數,將值累加到base上,也即其它線程正在初始化,多個線程正在更新base的值。

3. LongAdder.sum()

 

sum執行時,並沒有限制對base和cells的更新。所以LongAdder不是強一致性的,它是最終一致性的(也就是說併發情況下,sum的值並不精確)

首先,最終返回的sum局部變量,初始被複製爲base,而最終返回時,很可能base已經被更新了,而此時局部變量sum不會更新,造成不一致。其次,這裏對cell的讀取也無法保證是最後一次寫入的值。所以,sum方法在沒有併發的情況下,可以獲得正確的結果

四. 與AtomicLong對比

AtomicLong

  • 原理

CAS + 自旋

  • 場景

低併發下的全局計算,AlomicLong能保證併發情況下計數的準確性,其內部通過CAS來解決併發安全性的問題。可允許一些性能損耗,要求高精度時可使用。AtomicLong是多個線程針對單個熱點值value進行原子操作

  • 缺陷

高併發後性能急劇下降。(N個線程CAS操作修改線程的值,每次只有一個成功過,其它N-1失敗,失敗的不停的自旋直到成功,這樣大量失敗自旋的情況,佔用大量CPU)

LongAdder

  • 原理

CAS+Base+Cell數組分散,通過空間換時間分散了熱點數據

  • 場景

高併發下的全局計算,當需要在高併發下有較好的性能表現,且對值的精確度要求不高時,可以使用。LongAdder是每個線程擁有自己的槽,各個線程一般只對自己槽中的那個值進行CAS操作

  • 缺陷

sum求和後還有計算線程修改結果的話,最後結果不夠準確

原文鏈接:https://blog.csdn.net/weixin_43899792/article/details/124575032

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