LongAdder详解以及底层原理分析

一、原子累加器
我们都知道,原子整型可以在线程安全的前提下做到累加功能,而今天介绍的LongAdder具有更好的性能

我们先来看原子累加器和原子整型做累加的对比使用:

    private static <T> void demo(Supplier<T> supplier, Consumer<T> action){
        T adder = supplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        for (int i = 0;i<40;i++){
            ts.add(new Thread(()->{
                for (int j = 0;j<500000;j++){
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t->t.start());
        ts.forEach(t->{
            try {
                t.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
    }
 
    public static void main(String[] args) {
        for (int i = 0;i<5;i++){
            demo(()->new LongAdder(),longAdder -> longAdder.increment());
        }
        for (int i = 0;i<5;i++){
            demo(()->new AtomicLong(),atomicLong -> atomicLong.getAndIncrement());
        }
    }

 

通过上面的代码运行我们可以清晰地看到,两种方式都有效的产完成了累加的效果,但是明显使用累加器的效率要更好,甚至要高出原子类型累加好几倍。

现在,我们可以简单地理解为,原子累加器就是JAVA在并发编程下提供的有保障的累加手段。

二、LongAdder执行原理
LongAdder之所以性能提升这么多,就是在有竞争时,设置多个累加单元,不同线程累加不同的累加单元,最后在将其汇总,减少了cas重试失败,从而提高了性能。

简单来说,累加器就是将需要做累加的共享变量,分成许多部分,时多个线程只累加自己的部分(这样做既可以减少使用普通整型容易出现的线程不安全错误,也可以提高原子类型在累加时效率底下的问题),足以后再将结果汇总,得到累加结果。

就比如银行要清点大量钞票,一个人来清点效率低下,所以需要多个人来(多个线程),将大量钞票分给多个员工(分配累加单元),每个员工仅仅需要对自己的那部分钞票清点(每个线程累加自己的累加单元),最后将结果汇总起来,就是所有钞票的总数。

LongAdder中有几个关键域:

transient volatile Cell[] cells;//累加单元数组,懒惰初始化
transient volatile long base;//基础值,如果是单线程没有竞争,则用cas累加这个域
transient volatile int cellsBusy;//zaicell创建或扩容时,置位1,表示加锁


其中cells就是累加单元,用来给各个线程分配累加任务。

base用来做单线程的累加,同时还有汇总的作用,也是就是说base=cells[0]+cells[1]……+base

cellbase则用来表示加锁置位,0表示无锁,1表示加锁。

注意:
此处cellsbusy所说的锁并不是真正的对象锁,而是底层用cas来模拟加锁。

cas模拟加锁:

当占用某资源需要模拟加锁时,cas会将某处标志位的初始态变为加锁态(如将0改为1)。此时当出现竞争时,其他线程同样会尝试cas操作,但均以失败告终,所以会不停尝试cas,起到了加锁的功效。

@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
 
        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        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);
            }
        }
    }

 

在Cell类源码中我们能看出,cell底层也是采用cas来做累加计数。

三、伪共享
我们都知道cpu内存模型

因为 CPU 与内存的速度差异很大,需要靠预读数据至缓存来提升效率。而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64 byte (8个 long )

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。

CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

也就是说,在同一缓存行中的缓存的内容时时刻刻都是相同的,这样就违背了我们cells的初衷。

 

 

 

如图,

因为cells是数组类型,导致他们在内存中始终处于连续存储状态。

当任何一个线程改变cell中的值时,另一个线程中的缓存必然会失效(缓存是以行为单位进行更新),这样繁杂的操作使得效率大大降低。

问题解决
@ sun . misc . Contended 用来解决这个问题,

它的原理是在使用此注解的对象或字段的前后各增加128字节大小的 padding 从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

也就是说,增加无用的内存空间是cells扩容,从而在缓存中,每一个cell能占据一个缓存行,也就解决了失效的问题。

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