深入剖析LongAdder

LongAdder是JDK8添加到JUC中的。它是一个线程安全的、比Atomic*系工具性能更好的"计数器"。
它提供的方法主要有下面这些:

0 LongAdder的类图


LongAdder本身没有成员变量,其值的变更实际上是由父类Striped64管理的。
Striped64通过两个成员变量来管理value,分别是base和cells,cells是一个数组,其元素是Striped64的内部类Cell的实现,Cell很简单,只记录一个value。
当LongAdder不存在并发访问的时候,会直接通过cas的方式更新base的值,存在并发访问时,会定位到某一个cell,修改cell的value。
最终,value = base + sum(cells)。

1 深挖add方法

add方法的作用是将当前的value值 + x。其实弄懂add方法后,increment方法和decrement自然就明白了,这两个方法里面调的其实就是add方法。

1.1 add方法的实现

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // cells和base即Striped64维护的实例变量
    if ((as = cells) != null || !casBase(b = base, b + x)) { // #1
        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);
    }
}

初始状态下,cells==null,所以#1代码中,第一个条件一定是false,此时会通过casBase方法,以CAS的方式更新base值,且只有当cas失败时,才会走到if中。

啥时候会失败呢?存在竞争的时候。由此可以看出,当不存在竞争的时候,LongAdder是通过累加base值实现value的更新的。

当存在竞争的时候,LongAdder会尝试定位到其中的一个Cell,通过更新这个Cell的值来维护整体的value。这个过程如下:

cells==null时的处理在注释中已经标出。cellsBusy是用来控制并发修改cells的变量,初始值为0,通过casCellsBusy将cellsBusy的值从0更新为1。
然后,new了一个初始大小为2的Cell数组,new了一个Cell并将add的值作为初始值,然后计算一个cells的index,将这个Cell赋值进去。
最后,更新实例变量cells,并将cellsBusy重置为0。

cells!=null时,处理如下:

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) {
            // 从cells中定位一个Cell,如果是null,就new一个Cell,并将x作为初始值
            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
                
            // 如果定位到的Cell!=null,尝试通过cas的方式更新这个cell维护的value。
            // 如果更新成功,退出循环
            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;
            // 如果依然更新失败,扩容cells为原来的两倍,并将原来cells中的元素复制到新的cells中。
            // PS: 通过cellsBusy控制并发
            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()) {
            // ... 省略 ...
        }
        // 上述过程都失败,尝试更新base的值。
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

过程如下:

1.2 它为啥比AtomicLong更加高效

以AtomicLong的getAndAdd方法做一下对比。下面是这个方法的源码:

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the previous value
 */
public final long getAndAdd(long delta) {
    return unsafe.getAndAddLong(this, valueOffset, delta);
}

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

AtomicLong的实现是通过无限循环cas的方式更新当前维护的值,可以想象,在并发足够大的情况下,cas的失败率会很高,这里循环次数剧增,造成CPU使用率飙高。
通过上文分析LongAdder.add(int x)的原理,LongAdder先尝试一次cas更新,如果失败会转而通过Cell[]的方式更新值,如果计算index的方式足够散列,那么在并发量大的情况下,多个线程定位到同一个cell的概率也就越低,这有点类似于分段锁的意思。
由此也可以分析出另一点,在并发量不大的情况下,二者的性能是没有多大差异的。

2 深挖sum方法

sum方法用于返回LongAdder当前的值。

2.1 sum方法的实现

sum方法的实现很简单,其实就是 base + sum(cells)。源码如下:

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

2.2 为啥在并发情况下sum的值不精确

由上述源码可以发现,sum执行时,并没有限制对base和cells的更新。也就是说,对LongAdder的最后一次更新not happens-before 最近的一次读取。
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。
其次,这里对cell的读取也无法保证是最后一次写入的值。
所以,sum方法在没有并发的情况下,可以获得正确的结果。

3 深挖reset方法

用于初始化base和cells数组,将值重置为0。

3.1 reset方法的实现

源码如下:

public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                a.value = 0L;
        }
    }
}

其实就是将base和cells数组挨个设置成0。

3.2 为啥不可以在并发情况下使用

因为reset方法并不是原子操作,它先将base重置成0,假设此时CPU切走执行sum,此时的sum就变成了减去base后的值。
也就是说,在并发量大的情况下,执行完此方法并不能保证其他线程看到的是重置后的结果。所以要慎用。

4 总结

在实际工作中,可根据LongAdder和AtomicLong的特点来使用这两个工具。
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用LongAdder(例如网站访问人数计数)。
当需要保证线程安全,可允许一些性能损耗,要求高精度时,可以使用AtomicLong。

参考链接:深入剖析LongAdder是咋干活的

posted @ 2022-08-15 17:13  HKnight  阅读(295)  评论(0编辑  收藏  举报