LongAdder原始碼閱讀筆記

NicholasGUB發表於2021-06-12

功能描述

LongAdder通過建立多個副本物件,解決了多執行緒使用CAS更新同一個物件造成的CPU阻塞,加快了對執行緒處理的速度。當多個執行緒同一時刻更新一個AtomicLong型別的變數時,只有一個執行緒能夠更新成功,其他執行緒則更新失敗,繼續嘗試更新。

當使用LongAdder型別的變數時,由於副本陣列的存在,執行緒不一定直接更新變數的本身而是更新副本陣列,這樣多執行緒請求的物件變多了,從而減少了更新時間,當需要使用變數值時,返回的值是基礎變數的值加上陣列內每一個副本的值的和。

原始碼解析

LongAdder繼承自Striped64並實現了Serializable介面,而在Striped64類中有一個Cell類
image

add方法分析

首先從LongAdder類的add方法入手

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        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的實現主要依靠的是cells陣列,如果cells陣列為空的話,則嘗試使用cas更新基礎變數base,如果成功了,則add成功,方法結束,如果cas更新base失敗了,則證明此時有其他執行緒參與base變數的更新,此後的處理與cells不為空一致(如果cells不為空,則在此次方法執行前就已經有多執行緒參與了更新)。
當cells陣列不為空或者更新base變數失敗後,則轉而更新cells陣列中的副本,此時先判斷cells陣列是否為空或長度為0,如果為空或長度為0則說明這是第一次操作cells陣列,應先初始化cells陣列,因此呼叫方法longAccumulate(x, null, true);
如果cells陣列不為空,則嘗試直接訪問陣列中的副本,getProbe方法程式碼:

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

從上面程式碼可以看到getProbe方法獲取了當前執行緒內的PROBE變數,而PROBE定義在Striped64類中

    private static final long PROBE;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> sk = Striped64.class;
            BASE = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("base"));
            CELLSBUSY = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("cellsBusy"));
            Class<?> tk = Thread.class;
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

從上面的程式碼可以看出PROBE來自於Thread類的threadLocalRandomProbe變數,是一個執行緒級變數。getProbe() & m則是獲取當前要更新的cell,如果cell為空的話則呼叫longAccumulate(x, null, true);方法設定cell的值,如果cell不為空的話則使用cas直接更新cell的值,並將更新結果儲存在uncontended中,如果uncontended的值為false(即cas更新失敗了,此時應該有多個執行緒同時訪問了一個cell),那麼繼續呼叫longAccumulate(x, null, false);方法。

longAccumulate方法分析

從上面的add方法可以看到,getProbe()取得了Thread類中的threadLocalRandomProbe變數,而threadLocalRandomProbe變數的初始值為0,因為getProbe()方法參與了多執行緒訪問哪一個cell的定位,因此getProbe()的值不可能為0,那麼threadLocalRandomProbe變數是在哪裡賦值的呢?
在add方法中觀察到,沒當方法進行不下去時(base變數更新失敗,cells為空,cell更新失敗),都會呼叫longAccumulate方法,因此longAccumulate一定是涉及了cells陣列的初始化和擴容,觀察longAccumulate方法程式碼:

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                // 此處進行陣列的擴容
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }
            // 此處進行陣列的初始化
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

longAccumulate程式碼的設計非常複雜,剛進入方法的程式碼進行了threadLocalRandomProbe變數的初始化

int h;
if ((h = getProbe()) == 0) {
    ThreadLocalRandom.current(); // force initialization
    h = getProbe();
    wasUncontended = true;
}

如果getProbe()取得的值為0,說明threadLocalRandomProbe變數並未被初始化過,此時呼叫ThreadLocalRandom.current();方法進行初始化,並且將引數wasUncontended設定為true。ThreadLocalRandom.current()方法程式碼:

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

此時發現,threadLocalRandomProbe變數的初始化實在ThreadLocalRandom類中進行的,使用ThreadLocalRandom類的好處是為每一個執行緒維護一個隨機數種子,不涉及多執行緒競爭種子的問題。而在longAccumulate方法中初始化threadLocalRandomProbe變數是一種延遲初始化的操作,如果cells為空,即使threadLocalRandomProbe變數有值也是沒有意義的。

從上面add方法呼叫longAccumulate處可以發現,cells陣列為null或當前執行緒要更新的cell為null時wasUncontended的值為true,如果更新cell失敗,則cell的值為false,那麼wasUncontended的值一定是cells陣列進行擴容的依據。

因為是多個執行緒同時操作cells陣列,那麼對陣列的初始化和擴容一定只能由一個執行緒來完成,因此定義了cellsBusy變數,當檢測到cellsBusy的值為0並使用casCellsBusy()方法成功將其設定為1後才可以進行初始化和擴容操作。陣列的初始化將cells長度設定為2,並且初始化將要訪問的cell,另一個cell則保持預設值null,完成後將cellsBusy的值重新設定為0,方便其他執行緒之後進行擴容(此處設定並不是cas操作,因為當前初始化程式碼只有一個執行緒能執行到)。

而擴容要檢查當前cells陣列的長度小於cpu的個數時才可以進行(當陣列當都等於cpu個數時效率才最高),擴容操作完成後呼叫advanceProbe()方法從新計算threadLocalRandomProbe變數的值,以減少訪問celll衝突的個數。

另外,在定義Cell類時使用了@sun.misc.Contended註解,這樣保證了一個Cell類物件佔滿一個快取行,從而避免了偽共享問題,提升了效能。

相關文章