LongAdder In Java8

LongAdder In Java8

概述

LongAdder在多线程竞争较大的环境下,用于替代AtomicLong。两者底层都是使用CAS来update底层long变量的值,然而当多线程竞争较激烈的情况下时,AtomicLong会造成很多CPU空轮询(竞争太激烈,cas总是失败);而LongAdder采用将一个long变量的存储和更新分散到多个Cell中的做法:每个线程对应特定的一个cell,再进行更新时只更新本线程对应cell的long值,当需要获取累加值时,再通过将全部cell对应long值累加的方式,来减少线程间的竞争。

首先,将数据分段处理的做法并不是LongAdder实现的,而是它的父类Striped64实现的,基于此类还有LongAccumulator等。(LongAccumulator类提供了自定义Function的能力,比如可以传入一个做乘法的Function,即可实现Long的累乘器)。

源码解读

下面的文章推荐先通读一遍,对LongAdder和LongAccumulator有个初步了解,对于LongAdder是咋用的有了印象,再看具体实现。🤭

先从Striped64开始。上文说到该类将一个64位数据分散到多个cell中去,对应的就是Striped64的内部类Cell,类定义如下:

static final class Cell {
    // volatie保证可见性,如果是要存储double变量,则将double转化为对应的long(底层64位bit相同)
    volatile long value;
    Cell(long x) { value = x; }
    // cas操作保证多线程安全
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // 用于进行cas操作
    private static final sun.misc.Unsafe UNSAFE;
    // value变量在cell对象对应内存起始地址的偏移量,用于cas操作
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

然后是Striped64类中比较重要的几个field:

// 上述Cell类的数组,存储用于线程对应的Cell对象
transient volatile Cell[] cells;
// 逻辑cpu的数量,也是cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
// base变量,相当于Striped64类存储的一个默认Cell对象
// 当线程竞争不那么激烈时,对base值进行cas即可。因此在竞争不激烈时,该类等同于AtomicLong
transient volatile long base;
// cells数组对应的cas乐观锁,当需要对cells数组进行初始化、扩容时,需要先获得这个锁(即cas为1)
transient volatile int cellsBusy;

// 这几个变量用于cas操作,和Cell中的valueOffset类似
private static final sun.misc.Unsafe UNSAFE;
// base变量对于Striped64对象内存起始位置的偏移量
private static final long BASE;
// cellsBusy变量对于Striped64对象内存起始位置的偏移量
private static final long CELLSBUSY;
// base变量对于Thread对象内存起始位置的偏移量,该变量名为threadLocalRandomProbe,定义在Thread类中
// threadLocalRandomProbe变量可以认为是每个线程唯一的一个随机数,具体实现机制参考ThreadLocalRandom
private static final long PROBE;

有了cells对象,那么当多线程竞争时,每个线程都可以获取到自己对应的Cell对象来进行cas更新,相当于多个线程的竞争分散到了多个位置上。那么每个线程是如何获取自身对应的Cell对象的呢?

相关源码如下:

// 获取probe值
static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

// 调整probe值
static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

得到线程对于的probe值之后,再使用number & (cells.length - 1)的hash运算,来获取到对应cell对象的下标,在进行对cell对象的操作。

Striped64初始化时,底层的cells数组为null,因此再一开始没啥竞争的时候,通过对base变量的cas操作也就满足需求了。也就是下面这个:

final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

只有当发生多线程竞争时,才开始初始化cells数组,具体的方法是longAccumulate(long, LongBinaryOperator, boolean),对于double来说是doubleAccumulate方法,这里以long为例分析源码:

看这里的源码时需要注意的一点时,在条件语句中,虽然有可能条件判断结果是false,但是赋值语句已经执行了。这种写法在concurrent包下很常见。

// x是要通过fn对底层的long值进行操作的变量
// wasUncontended如果为true,表示没有竞争
final void doubleAccumulate(long x, LongBinaryOperator fn,
                                boolean wasUncontended) {
    int h;
    // 如果获取到的probe值为0,说明该Thread未初始化此值,则强制初始化一次
    // 具体的初始化流程可参考ThreadLocalRandom
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current();
        h = getProbe();
        // 新来的线程,乐观的认为有很大几率申请到自己独属的Cell对象或者casBase成功,则先置为没竞争
        wasUncontended = true;
    }
    // 表示当前cells数组是否已满
    boolean collide = false;      
    // cas无限循环
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // 1.cas已初始化
        if ((as = cells) != null && (n = as.length) > 0) {
            // *************************************1-1*************************************
            // 获取当前线程对应的Cell对象,如果为空
            if ((a = as[(n - 1) & h]) == null) {
                // cells锁
                if (cellsBusy == 0) {
                    // 创建新的Cell
                    Cell r = new Cell(x); 
                    // 通过cas来获取cells数组锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 二次检查,这里避免在进入该分支之后cells已经被update过
                            // 或者hash出来的位置已经被填充
                            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;
            }
            // *************************************1-1*************************************
            
            // *************************************1-2*************************************
            // 能进入这个分支则wasUncontented=false,也就是说认为有竞争
            // 认为有竞争的话就先不要去和其他线程竞争了,先等一回合
            // 下一回合应该就没竞争了, 即wasUncontented=true
            
            // 如果进不来这个分支的话,就说明认为没竞争(实际上有竞争也该去尝试了)
            else if (!wasUncontended)       
                wasUncontended = true;
            	/*
            	 * 这里变更竞争标识之后,会直接到最下面的h = advanceProbe(h),即调整probe变量值,
            	 * 重新进行hash计算。下面的分支同理。
            	*/
            // *************************************1-2*************************************
            
            // *************************************1-3*************************************
            // 存在竞争,通过hash获取的Cell对象不为空
            // 尝试使用已存在的Cell对象来进行cas操作
            // 如果操作成功,则break循环
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            // *************************************1-3*************************************
                
                
            // *************************************1-4*************************************
            // 存在竞争、通过hash获取的Cell对象不为空、对Cell进行cas失败
            
            // cells数组已经到了最大容量,或者cells数组不等于前面获取到的as局部变量(调整过length)
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            // *************************************1-4*************************************
            
            
            // *************************************1-5*************************************
            /*
                 * 能走到这个位置说明上面几个条件测试都失败了,也就是以下几点:
                 * (1)hash获取到的slot不为空
                 * (2)存在竞争
                 * (3)对获取到的Cell对象进行cas失败
                 * (4)cells数组长度小于cpu数量或者cells数组没更新
                 *
                 * 如果之前认为cells数组未满,则此时此刻可以认为cells数组已经被占满了,需要对cells数组
                 * 进行扩容
                 *
                 * 到下一轮循环之后,如果上述四个条件还满足(前面的条件全都测试失败),就会进入下面的cells
                 * 数组扩容环节需要注意的是,如果当前数组的长度n已经通过扩容到达了NCPU,那么之后在也不会进
                 * 行扩容了,因为每次都会进入到上面n >= NCPU的条件分支去
            */
            else if (!collide)
                collide = true;
            // *************************************1-5*************************************
            
            
            // *************************************1-6*************************************
            // 通过cas获取cells数组的锁
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    // double check,避免cells数组长度已经更新
                    if (cells == as) {     
                        // 扩展到2倍
                        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
            }
            // *************************************1-6*************************************
            
            
            // 调整h的大小
            h = advanceProbe(h);
        }
        // 2.cas未初始化
        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;
        }
        // 3.在对cells进行操作的间隙,再次尝试用base变量来cas,如果成功则退出
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          
    }
}

到这里对Striped64的分析就结束了,那么再回头看LongAdder其实就简单多了,说白了,其实就是提供了一个累加或累减的工具类,实际的工作都是在父类中干的。再简单分析下LongAdder中比较重要的add(long)方法,源码如下:

public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        
        if ((as = cells) != null // cells数组有没初始化,即之前有没有竞争
            || !casBase(b = base, b + x)) {	// 先通过casBase尝试一把,能不能直接成功,如果成功的话就不走下面了
            // 乐观认为没竞争
            boolean uncontended = true;
           
            if (as == null // cells数组没初始化
                	|| (m = as.length - 1) < 0 	// cells数组长度小于等于0
                	|| (a = as[getProbe() & m]) == null || // 当前线程对应的Cell是null
                	// 对当前线程对应cell尝试进行cas,成功就退出,不成功就将uncontented置为false(认为有竞争)
                	// 进入父类的longAccumulate方法中
                	!(uncontended = a.cas(v = a.value, v + x)))	
                longAccumulate(x, null, uncontended);
        }
    }
总结

LongAdderAtomicLong都提供了多线程环境下long变量运算的线程安全,但是LongAdder类使用空间换时间的方法,来减少多线程之间的竞争,大大提高了多线程下long运算的效率,ConcurrentHashMap在java1.8中的实现也是类似的。

感觉不错的看官可以点个👍:)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值