LongAdder源码导读

1 AtomicLong 

AtomicLongjuc包下的原子类,在并发情况下计数操作使用AtomicLong可以保证数据的准确性。

下面是AtomicLong类的加1和减1操作的源码。

    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }

 
    public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

其核心就是调用unsafe类提供的getAndAddLong方法,追进去其源码如下:

//unsafe类的getAndAddLong方法源码
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;
}

这里var2是AtomicLong中的valueOffset,它代表的是AtomicLong中的value在内存的偏移量。这里通过var6获取其值,compareAndSwapLong是CAS操作,通过while操作来进行自旋来抢夺锁。但是在多线程竞争激烈的情况下,compareAndSwapLong可能会失败并且频繁的自旋操作会让cpu空转,从而会降低时间效率,这也是AtomicLong原子类的瓶颈所在。

2 AtomicLong和LongAdder的比较

通过上述的分析,我们知道AtomicLong之所以效率太低是因为竞争而导致的自旋使得cpu进行空转,为了解决这个问题,在Jdk1.8中中引入了一个LongAdder类(Doug Lea大神)来解决这个问题,在低更新争用下,这两个类具有相似的特征;而在高争用的情况下,这一类的预期吞吐量明显更高,而代价是空间消耗更高。

下面用一个简单的demo比较一下synchronizedAtomicLong、LongAdder和LongAccumulator的时间效率,后面会对LongAddr的源码进行分析。

public class Juc_LongAddrUp {
    private final static int _1W = 10000;
    private final static int THREAD_SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_SIZE);
        CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_SIZE);
        CountDownLatch countDownLatch3 = new CountDownLatch(THREAD_SIZE);
        CountDownLatch countDownLatch4 = new CountDownLatch(THREAD_SIZE);
        long startTime;
        long endTime;
        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByS();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println(clickNumber.number + "\t" + (endTime - startTime));

        // =================================

        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickLong();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println(clickNumber.atomicLong.get() + "\t" + (endTime - startTime));

        // =================================

        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByLongAddr();
                    }
                } finally {
                    countDownLatch3.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println(clickNumber.longAdder.sum() + "\t" + (endTime - startTime));

        // =================================

        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByAccumulator();
                    }
                } finally {
                    countDownLatch4.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println(clickNumber.longAccumulator.get() + "\t" + (endTime - startTime));


    }
}


class ClickNumber {
    int number = 0;
    public synchronized void clickByS() {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void clickLong() {
        atomicLong.getAndIncrement();
    }
    
    LongAdder longAdder = new LongAdder();
    
    public void clickByLongAddr() {
        longAdder.increment();
    }
    
    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
    public void clickByAccumulator() {
         longAccumulator.accumulate(1);
    }
}

在这个demo中,分别使用四种方式进行计数,每个测试都开了50个线程,并在每个线程中持续进行1000,000次加一操作,可以发现LongAdder方法的时间效率要远低于 synchronizedAtomicLong。

LongAdder源码导读

3.1 使用分段的方式降低并发的冲突

这里列出LongAdder的一张原理图

在设计思想上,LongAdder采用分段的方式来降低并发冲突的概率。

AtomicLong中,实际存储数据的是一个value变量,所有的操作都会围绕该变量进行,也就是说高并发的情况下,多个线程会同时争抢该变量,获得成功的会进行操作,而失败的会不断进行自旋。

LongAdder的设计思路就是分散热点,即将value的操作分散到额外的数组,不同线程命中不同数组中的槽,各个线程只在自己命中的数据的槽中进行CAS操作,这样就会使得冲突大大减少。

 LongAdder继承了Striped64这个类,这个类中有一个全局变量transient volatile long base,在并发力度不是特别高的情况下,CAS是直接通过操作该base的值进行的,如果CAS操作失败,则代表并发冲突,这时会额外使用Cell[]数组中的cell单元格来进行CAS操作, 从而减少冲突的概率。

而最后对LongAdder获取值的方式也是通过base+Cell数组所有单元格值的方式来统计LongAddr此时的值情况。

3.2 使用Contended注解来消除伪共享

在LongAdder的父类Striped64中存在一个transient volatile Cell[] cells数组,其长度始终是2的幂次方,其中在Striped64中定义的内部类Cell中,其用注解@sun.misc.Contended进行修饰,这个疏解可以进行缓填充,从而解决伪共享问题。伪共享会导致缓冲失效,缓存一致性开销变大。

@sun.misc.Contended static final class Cell 

伪共享是指多个线程同时读写同一个缓存行的不同变量时导致cpu缓存失效,尽管这些变量之间没有任何关系,但是由于在主存中临近,存在于同一个缓存行中,所以相互覆盖会导致频繁的缓存未命中,导致性能低下。

解决伪共享的方法一般都是使用直接填充,我们只需要保证不同线程访存的变量存在于不同的CacheLine即可,使用多余的字节进行填充可以做到这一点。

缓存行填充对于大多数原子来说是繁琐的,因为它们大多不规则分散在内存中,因此彼此之间不会有太大的干扰。但是,驻留在数组中的原子对象往往彼此相邻,因此在没有这种预防措施的情况下,通常会共享缓存行数据(对性能有巨大的负面影响)。

 3.3 源码解读

下面将进行LongAdder的源码分析来观察下为什么LongAddr的时间效率会那么高。

首先解释下LongAdder的父类,Striped64的一些属性方法的定义

  • base:类似于AtomicLong中的全局value,没有竞争的情况下直接数据累加到base,或者在cells扩容时,也需要将数据写入到base
  • collide:表示扩容意向,false表示一定不扩容,true表示可能扩容
  • cellsBusy:初始化cells或者扩容cells时需要获取的锁,0表示无锁,1表示其他线程持有
  • casCellsBusy():通过CAS操作,修改cellsBusy的值,CAS成功表示获取锁,返回true
  • NCPU:表示当前计算机CPU的数量,Cell数组扩容时会用到
  • getProbe(): 获取当前线程的hash值
  • advanceProbe(): 重置当前线程的hash

LongAdder的加减操作如下:

    public void increment() {
        add(1L);
    }
    
    public void decrement() {
        add(-1L);
    }

可以观察到LongAddr的加减操作都是调用了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);
        }
    }

这里as是对cells数组的引用,bbasem表示cells数组长度-1a表示命中的cell单元格

  1. (as = cells) != null:不为空表示直接对cells数组的某个槽位进行CAS操作;为空表示cells数组尚未初始化,直接在base上进行CAS操作
  2. 如果cells为空:先用casBase来判断是否可以进行与baseCAS操作,如果成功的话则直接跳出。如果casBase操作失败,则意味着同时有多个线程争抢、并发起冲突,需要对cells进行扩容

如果(as = cells) != null || !casBase(b = base, b + x)true,表示要么已经开始使用cells数组或者并发争抢base起冲突需要进行cells的使用,进入下面这部分代码进行判断

(as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
  1. as == null:表示cells数组为空
  2. (m = as.length - 1) < 0: 表示数组长度为0
  3. (a = as[getProbe() & m]) == null:数组不为空则判断该线程命中的Cell是否为null
  4. !(uncontended = a.cas(v = a.value, v + x)):如果数组不为空并且命中的Cell不为null,就尝试对命中的Cell进行cas操作,成功则直接跳出,失败则将uncounted置为false

总结进入longAcccmulate的条件:cells为null或长度为0、以及命中的Cell为null进入longAccumulate(x, null, uncontended); uncontended为true,而只有命中Cell后再次进行CAS操作失败后才会将uncontended设置为false;

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方法:

  1. cells数组为null,且当前线程竞争base失败
  2. cells数组不为null,但是当前线程命中的cells单元格为null
  3. cells数组不为null,且当前线程命中cell单元格也不为null,但是当前线程竞争cell单元格失败

方法的参数

  • long x:要加的值
  • LongBinaryOperator fn : 默认为null

  • boolean wasUncontended: 表示当前线程竞争cell单元格是否成功,true为成功、false为失败(只有cells数组不为null,且命中不为null但是竞争失败才为false,其他所有情况均视为true)

下面来正式分析longAccumulate方法:

private static final long PROBE;

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
    	//设置为false,表示不会扩容(这是默认的)
    	boolean collide = false;                // True if last slot nonempty
    
static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

getProbe()方法是获取当前线程的哈希值,具体方法是通过UNSAFE.getInt()实现的

如果当前线程的hash值h == 0,0与任何数取模都是0,所以会固定在数组的第一个位置,这里做了优化,使用ThreadLocalRandom.current()重新计算了一个hash,最后设置为wasUncontended为true是因为想重新计算当前线程hash后的槽位是否是可以竞争成功的。

下面会进入一个for循环的自旋操作,这里简要展示该部分:

for (;;) {
    Cell[] as; Cell a; int n; long v;
    //CASE1:
    if ((as = cells) != null && (n = as.length) > 0) {
 
    }
    //CASE2:
    else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    }
    //CASE3:
    else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))

}

只有cells不为null,并且长度大于0才会进入CASE1,由于CASE1过于复杂,这里最后讲解

CASE2:         

//CASE2
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    boolean init = false;
    try {                           // Initialize table
        //CASE2.1
        if (cells == as) {
            //初始化
            Cell[] rs = new Cell[2];
            rs[h & 1] = new Cell(x);
            cells = rs;
            init = true;
        }
    } finally {
        cellsBusy = 0;
    }
    if (init)
        break;
}

走到CASE2表示cells数组为null,这里通过cells == as判断当前的cells数组是否和以前一样(当前没有别的线程初始化cells数组,如果这个条件不成立表明cells数组已经被初始化),并且只有线程获取锁的时候才会执行CASE2(casCellsBusy())

进入后再通过cells == as判断有没有修改过cells,然后对其进行初始化,并将该线程命中的Cell单元格new一个Cell对象。

CASE3

else if (casBase(v = base, ((fn == null) ? v + x :
                            fn.applyAsLong(v, x))))
    break;  

进入到这则表明cells数组正在进行初始化,即获取cells锁失败。再次尝试能不能获取base的锁,在base上进行CAS操作,如果获取到直接在base上操作,否则继续自旋转。

CASE1实现原理:

对其进行一点点的拆分:

1)首先就是判断,命中的Cell单元格为null,首先cellsBusy == 0会判断是否别的线程持有cells数组的锁。如果没有的话则会创建Cell对象,然后尝试自己获取锁

                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;
                }

获取锁后判断下该位置是否为null,如果为null则可以将new出来的Cell对象放在对应位置。created=true表示创建成功,就可以结束自旋了。

collide = false表示,只有当前线程命中的cell单元格为null但是当前线程没有获得锁时才会执行,将collide 设为false表明没有扩容想法,也正符合实际情况。

2)如果命中Cell不为空且存在竞争,将wasUncountended设置为true,同时在最下面重新hash获取新的命中单元


else if (!wasUncontended)      
    wasUncontended = true;      
h = advanceProbe(h);

3)当cells数组的长度大于等于cpu的核数或者cells != as设置collide = false表明没有扩容意向。这里超过cpu的核数不再扩容是因为cpu的核数代表计算机的处理能力,当超过cpu的核数时,多出来的cell单元格没太大作用,反而占用空间。

else if (n >= NCPU || cells != as)
    collide = false;            // At max size or stale

4)如果扩容意向为false,就将collide设为true,之后执行advanceProbe方法重置哈希值。这里的操作是为了当collide为false时就不再执行后面的扩容操作了。

else if (!collide)
    collide = true;

5)这里面其实是扩容逻辑,首先判断当前有无线程加锁,如果没有线程加锁那就通过casCellsBusy()方法尝试加锁,加锁成功之后将cellsBusy设为1,里面有一个if语句if (cells == as)是为了判断当前的cells数组和原来的数组是不是同一个(防止其他线程已经扩容过了),之后扩容为原来数组的两倍,之后将旧数组中的值拷贝到新数组中去,设置cellsBusy为0释放锁,设置collide为false表明已经没有扩容意向了,之后自旋。

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
}

LongAdder的sum:当我们使用LongAdder作为计数器时,需要调用sum()方法来汇总数据

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;
}

内部实现就是将base所有不为null的cell单元格内数值求和

LongAdder的核心思想就是空间换时间,将一个热点数据分散成cells数组从而减小冲突,以此来提升性能。

如果对于并发要求不高但是对精确要求很高的情况下还是建议使用AtomicLong。

如果对于并发要求很高但是对精确要求不高的情况下建议使用LongAdder,比如点赞等(因为在高并发下如果有并发更新,则利用sum汇总时数据可能不准确)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值