一. 性能對比
阿里開發手冊推薦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