关于LongAdder的源码分析

累加器——LongAdder

性能高于AtomicLong的累加器

为什么LongAdder是一个比AtomicLong更加优秀的累加方法?

//AtomicLong底层累加操作
long var6;
do {
    var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

AtomicLong在累加操作上通过不断自旋直到成功的方式进行累加,当并发量大的时候,每一时刻仅仅只能有1个线程完成CAS操作,其他的线程都会进行自旋并重试。性能就在此处造成了浪费。

LongAdder通过分散热点的方式降低重试次数来达到优化的作用。

由测试可得当并发量越大时LongAdder性能就更优于AtomicLong

LongAdder用了什么优秀的思想?

代码为LongAdder部分源码

//累加单元
transient volatile Cell[] cells;
//竞争不激烈是直接使用的累加数据
transient volatile long base;
//cas锁
transient volatile int cellsBusy;
//常规的尝试判断并添加
//核心点为:@sun.misc.Contended防止缓存行伪共享
@sun.misc.Contended static final class Cell {}

热点分散

LongAdder内部保存volatile long base用于存储CAS操作成功后的值。volatile Cell[] cells保存在CAS操作失败后通过占有一个数组空间,将当前需要累加的值保存在cells数组中,最后在执行完毕后通过sum操作将basecells里面的所有值累加最后获取结果。因此这样大大减少了自旋带来的性能浪费。

Cells数组大小永远是2的幂次方,原因是Table of cells. When non-null, size is a power of 2源码备注是么写的,其次就是在LongAccumulate中对数组扩容时创建Cell[n << 1]也能体现。

伪共享

Cell类之前存在一个@sun.misc.Contended注解,这就是当前类的存储在内存处于伪共享状态。

在内存中数据的刷新都是以行为单位,当刷新时相邻数据将会被覆盖,那么在其余线程访问对应相邻数据时就会频繁导致缓存未命中而尝试去主存中寻找数据,从而降低性能的浪费。所以为了解决这个问题这希望保证每个单元行只能存储一条数据,这样就能有效避免误伤其他缓存的问题。那么可以尝试在当前数据的左右添加64个字节的空白数据,这样就能解决问题。

随着科技发展,现在的计算机对于伪共享技术并不会造成多大的影响

源码分析

源码寻找数组索引值时,都是类似于HashMap通过hash值与&数组长度来寻找对应的索引。具体为啥这么做就研究hash表,这里不深入。其次源码中数组索引均是通过这个方法,所以将不在可以说通过...获取索引并获取数组的值,而是使用坑位或数组坑位来代替。

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

关于方法:

  1. 调用的increment方法其实就是调用的add(1L)
  2. casBase方法就是调用的UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
  3. getProbe()获取一个随机的探测值(随机的哈希值),每个线程都会随机生成一个值且保证之后都是固定的。

关于参数:

  1. as:表示cells数组数据
  2. b:当前未修改的值
  3. v:探测对象cell的值
  4. m:当前数组的最大索引
  5. a:探测位置的对象
  6. uncontended:是否竞争

longAccumulate方法

首先先看下源码前面一段代码。

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

ADD方法可知,每个线程需要有一个随机生成的之后固定的探测值,用这个值与数组的长度操作能够每次都为其安排一个数组坑位进行后续坑位累加或填充操作。现在需要获取这个探测值,那么就需要尝试获取。但是当获取的探测值为0时那就说明了当前这个值没有初始化,需要通过ThreadLocalRandom.current()方法进行初始化。

{
    Remark:"为啥判断是0就是没有初始化?因为getProbe()获取的是threadLocalRandomProbe参数"
    DesEn:"Probe hash value; nonzero if threadLocalRandomSeed initialized"
    DesCn:"探测哈希值;如果 threadLocalRandomSeed 已初始化,则非零"
}

下面则是源码核心步骤分为以下三个判断

  • cell数组已经初始化
  • cell数组没有初始化且没有正在初始化
  • 尝试修改base值是否成功
for (;;) {
    Cell[] as; Cell a; int n; long v;
    if ((as = cells) != null && (n = as.length) > 0) {
    }
    else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    }
    else if (casBase(v = base, ((fn == null) ? v + x :
                                fn.applyAsLong(v, x))))
        break;                         
}
步骤一:cell数组已经初始化
if ((a = as[(n - 1) & h]) == null) {
    if (cellsBusy == 0) {       
        Cell r = new Cell(x);   
        if (cellsBusy == 0 && casCellsBusy()) {
            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;           
        }
    }
    collide = false;
}
//之后的代码查看下面


if (!wasUncontended)       
    wasUncontended = true;      

wasUncontended:执行add方法时尝试修改坑位已有值的时候失败,这里会变成false。那么这里将标记修改为进行竞争并修改Hash值继续循环。实际这段代码中wasUncontended仅仅作为标记,别的好像没啥用。


if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break;

尝试修改坑位值


if (n >= NCPU || cells != as) collide = false;

判断当前cells数组大小是否大于CPU核心数,或判断当前数组是否被扩容?如果数组大于CPU核心数大小或已经扩容将扩容意向设置为不想扩容。

为什么需要添加CPU核心数为判断依据?:核心代表了同一时刻最大可以有多少个线程同时工作,如果线程数大于核心数,那么多出来的部分必须等待并通过调度算法进行调度执行。为了能够分散热点尽可能的让同时执行的线程都有对应的坑位进行操作。及时线程很多终究同一时刻只有核心数的线程数在执行,所以无需创建更大的数组。


if (!collide) collide = true;

如果都走到这一步表明现在坑位有值,且当前坑位有其他线程在操作,且数组长度并大于等于CPU核心数。那么扩容操作就变得有必要了,那么这一步开始设置为需要扩容,并再之后的轮询中进行扩容操作。


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

开始进行扩容,扩容大小是2的幂也是在这里体现出来的


h = advanceProbe(h);

除了值修改成功退出循环,不然都会进行hash值的更新,此方法也是用来规避当前坑位的竞争而再进入下个循环时去竞争另一个坑位。

最终的流程图

步骤二:cell数组没有初始化且没有正在初始化

此流程专门用于初始化Cell数组使用

if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    boolean init = false;
    try {
        if (cells == as) {
            Cell[] rs = new Cell[2];
            rs[h & 1] = new Cell(x);
            cells = rs;
            init = true;
        }
    } finally {
        cellsBusy = 0;
    }
    if (init)
        break;
}

步骤三:尝试修改base值是否成功
if (casBase(v = base, v + x)) break;

原本是判断fn对象是否为null,然后根据结果来选择结果。但是add调用时就是传的NULL,所以简化下面代码。

当前数组没有初始化且有线程正在尝试初始化,则尝试对base进行CAS操作。

获取值

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与Cells数组的值进行累加就可以了。

那么AtomicLong还有必要使用吗?

答案是需要,LongAdder是通过空间换时间的方式来提高性能。再低并发情况下其实使用AtomicLong更加优秀一点。但是LongAdder依然很牛

结束语

初次学习难免有误,如果出错还请大佬点出,我会及时更正。
参考文献也是我很推荐的,不然小白我也写不出这篇博客

参考文献

面试官问我LongAdder,我惊了...

本文作者:Ch1ee

本文链接:https://www.cnblogs.com/daimourentop/p/16471070.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Ch1ee  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起