JUC中LongAdder类的累加原理

LongAdder类 的累加原理

  • JUC中的 LongAdder类 的主要作用是进行基本类型 long 的多线程累加求和操作
  • JUC中的 AtomicLong 类也可以实现线程安全的累加操作,并且这样的 原子类型 还能更丰富的功能,例如 线程安全的自增操作(incrementAndGet())、自定义的计算操作updateAndGet(IntUnaryOperator updateFunction)等等。

那么为什么JUC还要单独提供LongAddr类呢?

因为LongAdder类在做 累加 这方面是专业的,与AtomicLong 类 相比,LongAdder类的累加性能要高出许多倍。

性能比较

以下用一个多线程并发累加的案例来演示比较 LongAdder类AtomicLong类 的性能:

// 在此处实现了一个通用的比较方法,让 LongAdder对象 和 AtomicLong对象 都使用这同一个方法的流程,保证公平性。
// Supplier接口 和 Consumer接口 都是 函数式接口(即有且仅有一个抽象方法,但是可以有多个非抽象方法的接口)
// Supplier接口的抽象方法可作为提供者,不传入参数,但返回结果;
// Consumer接口的抽象方法可作为消费者,传入一个参数,但不返回结果。
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
    T adder = adderSupplier.get();
    List<Thread> ts = new ArrayList<>();

    for (int i = 0; i < 4; i++) {
        ts.add(new Thread(() -> {
            for (int j = 0; j < 500000; j++) {
                action.accept(adder);
            }
        }, "t_" + i));
    }
    long start = System.nanoTime();

    ts.forEach(Thread::start);
    ts.forEach((t) -> {
        try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }
    });

    LOGGER.info("累加结果: {}, 花费时间:{} ms", adder, (System.nanoTime() - start)/1000000);
}

// 使用
for (int i = 0; i < 5; i++) {
    demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
LOGGER.info("==============================");
for (int i = 0; i < 5; i++) {
    demo(() -> new LongAdder(), adder -> adder.increment());
}

结果:
累加结果: 2000000, 花费时间:49 ms
累加结果: 2000000, 花费时间:47 ms
累加结果: 2000000, 花费时间:42 ms
累加结果: 2000000, 花费时间:42 ms
累加结果: 2000000, 花费时间:33 ms
=============================
累加结果: 2000000, 花费时间:17 ms
累加结果: 2000000, 花费时间:6 ms
累加结果: 2000000, 花费时间:7 ms
累加结果: 2000000, 花费时间:7 ms
累加结果: 2000000, 花费时间:6 ms
    
从结果可以看出,两种累加方法的结果都是正确的,确实没有线程不安全问题,但是很明显,LongAdder类的累加性能更好。   

工作原理

那么 LongAdder类 内部是如何优化,来让其累加性能如此优异的呢?

LongAdder类的重要变量

// 累加单元数组, 因 Cell 类是静态内部类,所以自带 Load On Demand(懒加载)功能
// 且Cell 类上有@sun.misc.Contended,防止了缓存行的伪共享
transient volatile Cell[] cells;
   
// 基础值, 如果没有竞争, 则用 cas 将值累加这个域
transient volatile long base;
   
// 此变量时CAS锁的关键参数。值为0, 表示无锁,值为1, 表示有锁。
transient volatile int cellsBusy;

几个常用方法的功能概括

int getProbe()方法,用来获得线程对应的probe值,类似于hash值,是一种hash函数映射。
int advanceProbe(int probe)方法:用来重新生成一个probe值,为什么要重新生成呢?因为在之前的probe映射到的cell中累加,一直失败。

boolean casCellsBusy()方法:用来上cas锁 ——— 上锁成功则返回true,此时cellsBusy应为1;上锁失败则返回false,证明有其他线程正在使用锁;使用锁结束后,应该让cellsBusy = 0,变成无锁状态。

简单来说,就是 ”映射归约,分而治之”,类似于 MapReduce :

  • 当cells还没有初始化,且线程没有竞争时,直接用 boolean casBase(b = base, b + x) 将累加结果加到 base 上,累加成功则返回 true。

    • if ((as = cells) != null || !casBase(b = base, b + x)) {
          // 此代码块用来操作 cell[] ,可进行初始化,扩容,和线程映射到某个 cell 中
          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个累加单元数组 cell[],内部包含多个累加单元 cell

  • 这样,可以将不同的线程**映射到不同的 cell **上进行累加;

  • 最后将 base 和 cell[] 中每个 cell 的结果汇总,就是最终结果。

  • 这样它们在累加时操作的不同的 cell 变量,因此减少了 CAS 重试失败 而导致的空转及性能损耗。


cells 的扩容步骤
Cell[] rs = new Cell[n << 1]; // 先 new 一个双倍容量的cell数组
for (int i = 0; i < n; ++i)
    rs[i] = as[i]; // 再从旧 cell[] 中将 cell 依次复制到新cell[]中。
cells = rs; // 修改成员变量的引用,让旧cell[]能被GC。
CAS锁

用CAS实现锁的一种方法:

public class LockCas {
    // state = 0代表无锁,state = 1代表有锁。
    private AtomicInteger state = new AtomicInteger(0); // 原子类型,线程安全
    // 上锁
    public void lock() {
        while (true) {
            if (state.compareAndSet(0, 1)) break;
        }
    }
    // 解锁
    public void unlock() {
        log.debug("unlock...");
        state.set(0);
    }
}

附上核心方法 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
    }
}

你可能感兴趣的:(java,多线程,mapreduce)