日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

性能远超AtomicLong,LongAdder原理完全解读

發布時間:2024/9/30 编程问答 51 豆豆
生活随笔 收集整理的這篇文章主要介紹了 性能远超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

final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended) {// 線程 threadLocalRandomProbe 屬性值int h;if ((h = getProbe()) == 0) {// 初始化 Thread 的 threadLocalRandomProbe 屬性值ThreadLocalRandom.current(); // force initializationh = getProbe();wasUncontended = true;}boolean collide = false; // True if last slot nonemptyfor (;;) {Cell[] as;Cell a;// cells 數組大小int n;long v;// cells 數組已經初始化if ((as = cells) != null && (n = as.length) > 0) {// 當前線程對應數組槽的 Cell 對象為空if ((a = as[(n - 1) & h]) == null) {if (cellsBusy == 0) { // Try to attach new Cell// 初始化 Cell,這里存在競爭,可能多個線程都創建了 Cell 對象Cell r = new Cell(x); // Optimistically create// casCellsBusy() 更新 cellsBusy 為 1,通過 CAS 操作保證只有一個線程操作成功,cellsBusy() 方法相當于一個 spin lockif (cellsBusy == 0 && casCellsBusy()) {boolean created = false;try { // Recheck under lockCell[] rs; int m, j;if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) {// rs[j] 對象賦值rs[j] = r;created = true;}} finally {cellsBusy = 0;}// Cell 在數組中賦值成功,跳出循環if (created)break;continue; // Slot is now non-empty}}collide = false;}else if (!wasUncontended) // CAS already known to failwasUncontended = true; // Continue after rehash// CAS 操作成功,跳出循環else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break;// 數組范圍不能大于 JVM 可用核心數,cells != as 表示數組可能擴容else if (n >= NCPU || cells != as)collide = false; // At max size or stale// 多個線程出現碰撞,更新 collide = true,出現碰撞后并沒有直接擴容 cells 數組,而是重新 rehash,rehash CAS 失敗后才會擴容else if (!collide) collide = true;// 走到這里說明出現了數組碰撞,且自旋 rehash CAS 失敗,這時需要對數組擴容else if (cellsBusy == 0 && casCellsBusy()) { try {// 數組擴容,重置 cells 數組if (cells == as) { // Expand table unless staleCell[] rs = new Cell[n << 1];for (int i = 0; i < n; ++i)rs[i] = as[i];cells = rs;}} finally {cellsBusy = 0;}// 擴容完成后標記碰撞為 falsecollide = false;continue; // Retry with expanded table}// 重置線程 threadLocalRandomProbe 值,重新 rehash 用h = advanceProbe(h);}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {boolean init = false;try { // Initialize tableif (cells == as) {// 初始化 cells 數組,默認大小為 2Cell[] rs = new Cell[2];// 角標賦值rs[h & 1] = new Cell(x);cells = rs;init = true;}} finally {cellsBusy = 0;}if (init)break;}// 多個線程初始化,只有一個線程初始化成功,其他線程嘗試更新 base 值else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break; // Fall back on using base}}

下面是具體每一個分支的詳細說明:

  • 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 數組初始化完成是不是就不會再更新 base 值了?
  • 答:不會,可能有多個線程嘗試初始化 cells 數組,最終只有一個線程成功,失敗的線程還會以 CAS 的方式更新 base 值

  • cells 數組什么時候擴容?
  • 答:多個線程操作 cells 數組出現槽碰撞,碰撞后并不會直接擴容,而是修改線程的 threadLocalRandomProbe 值,以自旋的方式重新 rehash,如果還出現碰撞(此時 collide = true),則擴容 cells 數組

  • 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原理完全解读的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。