性能远超AtomicLong,LongAdder原理完全解读
高并發場景下可以選擇 AtomicLong 原子類進行計數等操作,除了 AtomicLong,在 jdk1.8 中還提供了 LongAdder。PS:AtomicLong 在 jdk1.5 版本提供。
AtomicLong 底層使用 Unsafe 的 CAS 實現,當操作失敗時,會以自旋的方式重試,直至操作成功。因此在高并發場景下,可能會有很多線程處于重試狀態,徒增 CPU 的壓力,造成不必要的開銷。
LongAdder 提供了一個 base 值,當競爭小的情況下 CAS 更新該值,如果 CAS 操作失敗,會初始化一個 cells 數組,每個線程都會通過取模的方式定位 cells 數組中的一個元素,這樣就將操作單個 AtomicLong value 的壓力分散到數組中的多個元素上。
通過將壓力分散,LongAdder 可以提供比 AtomicLong 更好的性能。獲取元素 value 值時,只要將 base 與 cells 數組中的元素累加即可。
下面是它的原理實現。
public void increment() {add(1L);}public void add(long x) {Cell[] as;long b, v;// m = as.length -1,取模用,定位數組槽int m;Cell a;// 低競爭條件下,cells 為 null,此時調用 casBase(底層為 CAS 操作,類似 AtomicLong) 方法更新 base// PS:cells 數組為懶加載,只有在 CAS 競爭失敗的情況下才會初始化if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;// as 數組為 null,或者數組 size = 0,或者計算槽后在數組中不能定位,或者 cell 對象 CAS 操作失敗if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);}}LongAdder 繼承自 Striped64,底層調用 Striped64.longAccumulate 方法實現。
當第一次調用 add 方法時,并不會初始化 cells 數組,而是通過 CAS 去操作 base 值,操作成功后就直接返回了。
如果 CAS 操作失敗,這時會調用 longAccumulate 方法,該方法會初始化 Cell 類型的數組,后面大部分線程都會直接操作這個數組,但是仍然有部分線程會更新 base 值。
Cell 元素定義如下:
@sun.misc.Contended static final class Cell {volatile long value;Cell(long x) { value = x; }final boolean cas(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);}// Unsafe mechanicsprivate static final sun.misc.Unsafe UNSAFE;private static final long valueOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> ak = Cell.class;valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));} catch (Exception e) {throw new Error(e);}}}多個線程操作 cells 數組原理如下:
圖片來源 LongAdder and LongAccumulator in Java
CPU 有多級緩存,這些緩存的最小單位是緩存行(Cache Line),通常情況下一個緩存行的大小是 64 字節(并不絕對,或者是 64 的倍數)。假設現在要操作一個 long 類型的數組,long 在 Java 中占 64 bit,8 個字節,當操作數組中的一個元素時,會從主存中將該元素的附近的其他元素一起加載進緩存行,即使其他元素你不想操作。
假設兩個用 volatile 修飾的元素被加載進同一個緩存行,線程 A 更新變量 A 后會將更新后的值刷新回主存,此時緩存行失效,線程 B 再去操作 B 變量只能重新從主存中讀取(Cache Miss)。這就造成了偽共享(False sharing)問題。
Cell 本身沒什么好講的,仔細看一下,這個類被 @sun.misc.Contended 注解修飾,這個注解一般在寫業務時用不到,但是它可以解決上面的偽共享問題。
@sun.misc.Contended 注解在 jdk1.8 中提供,保證緩存行每次緩存一個變量,剩余的空間用字節來填充。
圖片來源 LongAdder and LongAccumulator in Java
下面是具體每一個分支的詳細說明:
- cells 數組不為空,嘗試更新數組中的 Cell 元素
- 如果當前線程對應數組中的槽沒有 Cell 元素,則初始化一個 Cell 元素,加鎖成功后將初始化的 Cell 存在數組對應的槽中,跳出循環,槽位置 = thread.threadLocalRandomProbe & cells.length - 1,這里 & 操作相當于 %
- 如果 wasUncontended 為 false,表示 CAS 操作失敗,操作失敗后會重置線程的 threadLocalRandomProbe 屬性,自旋時會重新 rehash
- CAS 操作當前數組槽對應的 Cell,累加操作的變量值,累加成功跳出循環,失敗重置線程的 threadLocalRandomProbe 屬性,自旋時會重新 rehash
- cells 數組可能擴容,數組長度不能大于 JVM 的可用核心數,如果擴容,或者數組已經達到最大容量,將 collide 值置為 false
- 這里補充說明一下,collide 為碰撞的意思,指的是多個線程經過 hash 后對應數組中的槽是否出現碰撞
- 如果 cells 數組已經擴容到了最大限制,即使出現碰撞也不會再擴容 cells 數組了,因此將 collide 值置為 false
- cells != as 表示數組出現了擴容,此時忽略碰撞情況,也將 collide 值置為 false
- 如果 collide 為 false,將 collide 置為 true,意味著這此時已經出現了碰撞,出現碰撞并不會直接擴容 cells 數組,而是更新線程 threadLocalRandomProbe,自旋時重新 rehash,rehash CAS 失敗后才會擴容
- 如果出現了碰撞,且 rehash 后 CAS 更新 Cell 失敗,進行加鎖,加鎖成功對 cells 數組擴容
- cells 數組還沒有初始化,且線程加鎖成功,則初始化 cells 數組容量為 2,且將當前線程對應的 value 值封裝成 Cell 元素,存儲 cells 數組中
- 可能有多個線程嘗試初始化 cells 數組,但最終成功的只有一個,其他初始化失敗的并不會以自旋的方式操作 cells 數組,而是嘗試通過 CAS 去操作 base 值,因此在 cells 數組初始化完成之后,也是有可能是修改 base 值的
到這里 LongAdder 的原理就介紹完了,這時再來看以下幾個問題?
答:不會,可能有多個線程嘗試初始化 cells 數組,最終只有一個線程成功,失敗的線程還會以 CAS 的方式更新 base 值
答:多個線程操作 cells 數組出現槽碰撞,碰撞后并不會直接擴容,而是修改線程的 threadLocalRandomProbe 值,以自旋的方式重新 rehash,如果還出現碰撞(此時 collide = true),則擴容 cells 數組
答:上面代碼過程中有一個 else if (n >= NCPU || cells != as) 判斷,這個 NCPU 表示 JVM 可用核心數,NCPU = Runtime.getRuntime().availableProcessors(); 。注意這個 JVM 可用核心數并不一定等于 CPU 核心數,比如我的電腦是 6 核,JVM 可用核心數是 12。else if (n >= NCPU || cells != as) 意味著數組的容量不能大于 JVM 的可用核心數,假設一個服務器 JVM 可用核心數為 6,由于數組每次擴容 2 倍,第一次初始化時為 2,那最大容量應該為 4。其實不是這樣的,因為這個判斷是在擴容前進行的,假設此時數組容量為 4,由于可用核心數為 6,條件判斷通過,且存在碰撞情況,那么還是會擴容 cells 的容量為 8。因此我認為 cells 數組的最大容量為第一個大于 JVM 可用核心數的 2 的冪。
如果以上分析有錯誤分歧,歡迎大家在下面留言交流指正。
參考
LongAdder and LongAccumulator in Java
A Guide to False Sharing and @Contended
How do cache lines work?
JAVA 拾遺 — CPU Cache 與緩存行
Java并發工具類之LongAdder原理總結
總結
以上是生活随笔為你收集整理的性能远超AtomicLong,LongAdder原理完全解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 工银e生活里面怎么绑定储蓄卡还信用卡
- 下一篇: 银行卡开户地是什么意思