LongAdder源码导读

1,048 阅读7分钟

前言

JDK8提供了一个性能更好的长整型原子类:java.util.concurrent.atomic.LongAdder。 在高并发下,它有着比AtomicLong更好的性能,代价是会消耗更多的内存空间。

有了AtomicLong,为啥还要引入LongAdder? AtomicLong是通过volatile+CAS的方式来保证并发安全的,它只有一个value值,所有的线程都对这一个value值操作,也就意味着这个value是热点数据。随着并发量越来越高,CAS操作失败的概率也会越来越大,会有越来越多的线程自旋重试,干耗CPU资源,降低性能。关于AtomicLong的详细解释,可以看笔者的另一篇文章:《AtomicLong源码导读》。

为了解决高并发量下AtomicLong的性能问题,Doug Lea他老人家写了个LongAdder。需要说明的是,LongAdder无法完全替代AtomicLong,LongAdder的sum()获取的是一个估计值,要想获取某一时刻的准确值,需要加全局锁。

LongAdder是如何优化的? LongAdder的代码比AtomicLong复杂的多,它主要的思想还是:分而治之。 LongAdder将AtomicLong中唯一的热点数据进行了分散,它用到了Cell[]数组,每个Cell都是一个资源,线程只操作自己Cell的value,大大减少了热点资源的争用,这和ConcurrentHashMap的分段锁是一个思想。

通过分而治之的思想,将单个热点资源分散到Cell[]数组中,线程只操作自己Cell的value,最终汇总结果时,只需将每个Cell的value累加就是LongAdder的最终结果。

需要注意的是,由于汇总结果时,仍然有线程在对Cell的value进行修改,因此sum()返回的结果只是一个估计值,并不准确。如果需要计算准确值,需要加全局锁,因此LongAdder更适合写多读少的场景。


源码导读

UML

在这里插入图片描述 LongAdder的结构并不复杂,和AtomicLong的区别是,它继承了java.util.concurrent.atomic.Striped64类。Striped64采用了分段锁的思想,它主要用来在高并发下高效的处理64位的数据。 Striped64的设计思想是在高并发下,将热点资源的争用尽量做到分散。在没有争用的情况下,直接操作base变量,和AtomicLong的效果一样。存在多线程争用的情况下,则将热点资源分散到Cell[]数组中。 每个Cell实例都是一个资源,内部维护了一个volatile long value,同时提供了cas()方法,允许线程对value进行CAS修改。

属性

LongAdder本身没有属性,全都继承自父类Striped64。 base是一个被volatile修饰的长整型,它的作用有两个:

  1. 没有资源争用的情况下,直接操作base即可。
  2. Cell[]初始化的时候,将操作回退到base身上。

cells就是用来分散热点资源的,每个Cell实例本身就可以看作是一个AtomicLong,LongAdder会将Thread的threadLocalRandomProbe属性视为线程的哈希值,然后通过hash & (cells.length-1)计算下标,每个线程只操作自己对应下标的Cell实例,减少热点资源的争用。

cellsBusy可以看做是Striped64内部的一个锁标志,0代表无锁,1代表有锁,通过CAS的方式去修改它。主要是在Cell[] cells创建、扩容时会加锁。

/**
 * Cell数组,减少热点资源用的。
 */
transient volatile Cell[] cells;

/**
 * 1.没有资源争用的情况下,直接操作base即可。
 * 2.cells初始化的时候,将操作回退到base身上。
 */
transient volatile long base;

/**
 * 锁标志,0代表无锁,1代表有锁
 */
transient volatile int cellsBusy;

核心方法

add

调用add()方法时,LongAdder首先会判断Cell[]是否已创建,如果没有创建,说明不存在资源争用,则直接调用casBase()去修改base变量即可,如果CAS失败说明存在资源争用,则要去创建Cell[],将热点资源分散开,避免过多的自旋。

如果Cell[]创建了说明已经存在资源争用了,则线程去操作属于自己的Cell即可。

/**
 * 添加给定的值x
 */
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 如果cells没创建,则直接CAS尝试直接修改base变量
    if ((as = cells) != null || !casBase(b = base, b + x)) {
    	// 存在资源争用的情况
        boolean uncontended = true;
        // cells没创建
        if (as == null || (m = as.length - 1) < 0 ||
        	// 获取当前线程对应的Cell
            (a = as[getProbe() & m]) == null ||
            // cells已经创建,且当前线程对应的Cell已创建,则直接CAS操作对应的Cell
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

大致流程图如下: 在这里插入图片描述 如果存在资源争用的情况,会调用Striped64的longAccumulate()方法,逻辑还是挺复杂的:

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;//线程的哈希码
    if ((h = getProbe()) == 0) {
    	// 如果线程的哈希码为0,说明没有被初始化
        ThreadLocalRandom.current();// 强制初始化,计算threadLocalRandomProbe
        // 获取线程的哈希码,即Thread.threadLocalRandomProbe
        h = getProbe();
        wasUncontended = true;
    }
    // 是否发生哈希碰撞:不同线程映射到了同一个Cell
    boolean collide = false;
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // cells已经完成初始化的情况
        if ((as = cells) != null && (n = as.length) > 0) {
        	/*
        	(n - 1) & h就是取线程对应的Cell下标,和HashMap玩法一样
        	线程对应的Cell为null,则要创建Cell,加锁,放入cells中。
			*/
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {       // 无锁时才能能操作
                    Cell r = new Cell(x);   // 创建Cell实例
                    // 尝试加锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                    	// 加锁成功,将Cell实例放入cells,
                        boolean created = false;
                        try {// 加锁后再重新检查一遍
                            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;
            }
            // wasUncontended代表上一次CAS操作是否成功
            else if (!wasUncontended)
            	// CAS失败,则重置为true,后面重新计算哈希码
                wasUncontended = true;
            // 线程对应的Cell不为null,则尝试修改Cell的value值,成功则直接返回
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            // 当Cell[]长度达到CPU核心数,就不会再扩容了,计算密集型,再扩容意义不大
            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) {
                    	// 两倍扩容
                        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
            }
            // 重新计算当前线程的哈希码,并写入Thread.threadLocalRandomProbe
            h = advanceProbe(h);
        }
        // Cell[]未初始化的情况:尝试加锁并初始化
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    Cell[] rs = new Cell[2];// 默认长度是2
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
            	// 解锁
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // Cell[]正在初始化中,操作直接在base上完成
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

longAccumulate()主要有三种情况:

  1. Cell[]未初始化,则要加锁并初始化,默认长度为2。
  2. Cell[]在初始化的过程中,操作退回到base变量中去完成,暂时直接修改base即可。
  3. Cell[]初始化完成,则判断和当前线程绑定的Cell是否已创建,如果没创建则加锁创建,并放入cells当中,如果已创建则直接操作Cell.cas(),如果失败则说明多个线程在竞争同一个Cell,需要进行扩容操作。如果数组长度已经达到CPU的核心数,则不会再扩容了。

sum

由于添加的值被分散到了Cell[]中,因此求和时,需要将baseCell[]中的每一项value做统计。

/*
由于求和时,其他线程仍在操作Cell,因此统计的结果只是一个估计值。
要想计算准确值,必须加全局锁。
*/
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;//以base为基数,依次累加所有Cell的value即可
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

其他像increment()decrement()longValue()其实就是变相的调用了add()sum(),这里就不作解释了。

总结

AtomicLong通过一个变量value作为long的实际值,所有线程的操作都会争用这一个资源,因此value会成为唯一的热点资源,并发量高时,会导致大量的CAS失败,线程会进行多次自旋重试,影响性能。

LongAdder通过分而治之的思想,将热点资源分散到了baseCell[]中,当没有资源争用时,直接操作base即可,尽可能的将Cell[]的创建延迟。当确实发生资源争用时,才创建Cell[],默认长度是2,扩容规则是两倍,且最大长度是CPU的核心数。将Thread的threadLocalRandomProbe视为哈希码,通过hash & (cells.length-1)定位到Cell,每个线程只操作自己的Cell,降低资源的争用情况。当Cell还有剩余,但是Thread的哈希码冲突时,LongAdder会调用advanceProbe()重新计算Thread的哈希码,并写回到Thread.threadLocalRandomProbe

分而治之分段锁是LongAdder的核心思想。