原子操作增强类LongAdder

一. 性能对比

阿里开发手册推荐jdk8使用LongAdder替代AtomicLong

 

 

 

 

示例代码
题目:热点商品点赞计算器,点赞数加加统计,不要求实时精确。50个线程,每个线程100W次,统计总点赞数

比较synchronized、AtomicInteger、AtomicLong、LongAdder、LongAccumulator五种计数性能

class ClickNumber{
    int number = 0;
    public synchronized void add_synchronized(){
        number++;
    }
 
    AtomicInteger atomicInteger = new AtomicInteger();
    public void add_AtomicInteger(){
        atomicInteger.incrementAndGet();
    }
 
    AtomicLong atomicLong = new AtomicLong();
    public void add_AtomicLong(){
        atomicLong.incrementAndGet();
    }
 
    LongAdder longAdder = new LongAdder();
    public void add_LongAdder(){
        longAdder.increment();
    }
 
    LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
    public void add_longAccumulator(){
        longAccumulator.accumulate(1);
    }
}
 
public class LongAdderCalcDemo {
    public static final int SIZE_THREAD = 50;
    public static final int _1w = 10000;
 
    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime = System.currentTimeMillis();
        long endTime = System.currentTimeMillis();
        CountDownLatch latch_synchronized = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_AtomicInteger= new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_AtomicLong = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_LongAdder = new CountDownLatch(SIZE_THREAD);
        CountDownLatch latch_LongAccumulator = new CountDownLatch(SIZE_THREAD);
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_synchronized();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_synchronized.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_synchronized.await();
        endTime = System.currentTimeMillis();
        System.out.println("synchronized花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.number);
 
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_AtomicInteger();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_AtomicInteger.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_AtomicInteger.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicInteger花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.atomicInteger.get());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_AtomicLong();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_AtomicLong.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_AtomicLong.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicLong花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.atomicLong.get());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_LongAdder();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_LongAdder.countDown();
                }
            },String.valueOf(i)).start();
        }
        latch_LongAdder.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAdder花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.longAdder.longValue());
 
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= SIZE_THREAD; i++) {
            new Thread(()->{
                try {
                    for (int j = 1; j <= 100*_1w; j++) {
                        clickNumber.add_longAccumulator();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch_LongAccumulator.countDown();
                }
 
            },String.valueOf(i)).start();
        }
        latch_LongAccumulator.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAccumulator花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.longAccumulator.longValue());
 
    }
}

 

 

 


通过结果,可知LongAdder性能最优,花费时间最短,远优于AtomicLong

二. LongAdder为何那么快

LongAdder的基本思路

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。sum()会将所有Cell数组中的value和base累加作为返回值,

核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。

LongAdder的原理


LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作(无并发,单线程下直接CAS操作更新base值;非竞态条件下,直接累加到变量base上)

当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组Cells.多个线程需要同时对value进行操作时,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。(有并发,多线程下分段CAS操作更新Cell数组值;竞态条件下,累加个各个线程自己的槽Cell[]中)

 

 

 

 

sum()公式

 

 

 

三. 源码解析

1. LongAdder.add()

 

 

 

Cell[] as; long b, v; int m; Cell a;

as是striped64中的cells数组属性
b是striped64 中的base属性 v是当前线程hash到的Cell中存储的值
m是cells的长度减1,hash时作为掩码使用
a是当前线程hash到的Cell

if ((as = cells) != null || !casBase(b = base, b + x))

条件1: cells不为空,说明出现过竞争,Ccell[]已创建
条件2: cas操作base失败,说明其它线程先一步修改 了base正在出现竞争

首次首线程((as = cells) != null)一定是false,此时走casBase方法,以CAS的方式更新base值,且只有当cas失败时,才会走到if中

boolean uncontended = true

true无竞争,false表示竞争激烈,多个线程hash到同一个cell,可能要扩容

if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))

条件1: cells为空,说明正在出现竞争,上面是从条件2过来的
条件2: 应该不会出现
条件3: 当前线程所在的cell为空,说明当前线程还没有更新过cell,应初始化一个cell
条件4: 更新当前线程所在的cell失败,说明现在竞争很激烈,多个线程hash到了同一个个Cell,应扩容

longAccumulate(x, null, uncontended)

调用striped64中的方法处理

小结

  • 1.最初无竞争时只更新base;
  • 2.如果更新base失败后,首次新建一个Cell[]数组
  • 3.当多个线程竞争同一个CelI比较激烈时,可能就要对Cell[]扩容

2. Striped64.longAccumulate()

longAccumulate()方法的入参

long X需要增加的值,一般默认都是1
LongBinaryOperator fn默认传递的是null
wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化之后, 并且当前
失败,才会是false

 

 

 

 

代码解释

上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
CASE1: Cell[]数组已经初始化

  


CASE2: Cell[]数组未初始化(首次新建)

 


如果上面条件都执行成功就会执行数组的初始化及赋值操作

Cell[] rs = new Cell[2]表示数组的长度为2;rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1;h & 1类似于HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1),同hashmap一个意思

CASE3: Cell[]数组正在初始化中

 

  

多个线程尝试CAS修改失败的线程会走到这个分支,该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化,多个线程正在更新base的值。

3. LongAdder.sum()

 

sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的(也就是说并发情况下,sum的值并不精确)

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果

四. 与AtomicLong对比

AtomicLong

  • 原理

CAS + 自旋

  • 场景

低并发下的全局计算,AlomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。可允许一些性能损耗,要求高精度时可使用。AtomicLong是多个线程针对单个热点值value进行原子操作

  • 缺陷

高并发后性能急剧下降。(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,占用大量CPU)

LongAdder

  • 原理

CAS+Base+Cell数组分散,通过空间换时间分散了热点数据

  • 场景

高并发下的全局计算,当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用。LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

  • 缺陷

sum求和后还有计算线程修改结果的话,最后结果不够准确

原文链接:https://blog.csdn.net/weixin_43899792/article/details/124575032

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章