关于java:突击并发编程JUC系列​JDK18-扩展类型LongAdder

40次阅读

共计 2938 个字符,预计需要花费 8 分钟才能阅读完成。

突击并发编程 JUC 系列演示代码地址:
https://github.com/mtcarpenter/JavaTutorial

小伙伴们,大家好,咱们又见面了,突击并发编程 JUC 系列实战 JDK1.8 扩大类型马上就要发车了。

JDK 1.8 扩大类型如下

初步理解

后面在解说 AtomicLong, 跟大家提到过longAdder , AtomicLong 通过 CAS 提供了非阻塞的原子性操作,相比应用阻塞算法的同步器来说它的性能曾经很好了,然而 JDK 开发组并不满足于此。应用 AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,然而因为同时只有一个线程的 CAS 操作会胜利,这就造成了大量线程竞争失败后,会通过有限循环不断进行自旋尝试 CAS 的操作,而这会白白浪费 CPU 资源。

JDK 8 开始,针对 Long 型的原子操作,Java又提供了 LongAdderLongAccumulator;针对Double 类型,Java提供了 DoubleAdderDoubleAccumulatorStriped64 相干的类的继承档次如下所示。

LongAdder克服了高并发下应用 AtomicLong 的毛病。既然 AtomicLong 的性能瓶颈是因为过多线程同时去竞争一个变量的更新而产生的,LongAdder则是把一个变量合成为多个变量,让同样多的线程去竞争多个资源,解决了性能问题。

应用 AtomicLong 时,是多个线程同时竞争同一个原子变量。图示如下

应用 longAdder 多个线程同时竞争一个原子变量,图示如下

LongAdder 是把一个变量拆成多份,变为多个变量,有点像 ConcurrentHashMap 中 的分段锁 把一个 Long 型拆成一个 base 变量外加多个 Cell,每个Cell 包装了一个 Long 型变量。这样,在等同并发量的状况下,抢夺单个变量更新操作的线程量会缩小,这变相地缩小了抢夺共享资源的并发量。
另外,多个线程在抢夺同一个 Cell 原子变量时如果失败了,它并不是在以后 Cell 变量上始终自旋 CAS 重试,而是尝试在其余 Cell 的变量上进行 CAS 尝试,这个扭转减少了以后线程重试 CAS 胜利的可能性。最初,在获取 LongAdder 以后值时,是把所有 Cell 变量的 value 值累加后再加上 base 返回的。LongAdder保护了一个提早初始化的原子性更新数组(默认状况下 Cell 数组是 null)和一个基值变量base。因为Cells 占用的内存是绝对比拟大的,所以一开始并不创立它,而是在须要时创立,也就是惰性加载。

案例测试

上面通过 AtomicLong 和 LongAdder 别离对百万雄师求和,为了更好的对别离通过 10、100、500 个线程并发求和百万雄师数量。

AtomicLong 性能测试

public class AtomicExample10 {

    // 并发线程数
    public static int requestTotal = 500;
    // 求和总数
    public static int sumTotal = 1000000;

    public static AtomicLong count = new AtomicLong(0);


    public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(requestTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        long start = System.currentTimeMillis();
        for (int i = 0; i < requestTotal; i++) {executorService.execute(() -> {add();
                countDownLatch.countDown();});


        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("count=" + count.get());
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }

    private static void add() {
        // 针对 sumTotal 求和
        for (int j = 0; j < sumTotal; j++) {count.getAndIncrement();
        }

    }
}

通过 10、100、500 个并发线程测试

 // 并发线程数 10
count=10000000
耗时:305
// 并发线程数 100
count=100000000
耗时:2301
// 并发线程数 500
count=500000000
耗时:10865

LongAdder 性能测试

public class AtomicExample11 {

    // 申请总数
    public static int requestTotal = 100;

    public static LongAdder count = new LongAdder();


    public static void main(String[] args) throws InterruptedException {final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        long start = System.currentTimeMillis();
        for (int i = 0; i < requestTotal; i++) {new Thread(() -> {
                try {TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                add();
                countDownLatch.countDown();}).start();}
        countDownLatch.await();
        System.out.println("count=" + count);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }

    private static void add() {count.add(1);
    }

}

通过 10、100、500 个并发线程测试

// 并发线程数 10
count=10000000
耗时:110
// 并发线程数 100
count=100000000
耗时:375
// 并发线程数 500
count=500000000
耗时:1451

总结

在以上的测试并发数越多 LongAdder性能越突出,LongAdder 是把一个变量拆成多份,扩散到多个变量,通过外部 cells 数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程能够同时对 cells 数组外面的元素进行并行的更新操作,其外围实现通过空间来换工夫。


欢送关注公众号 山间木匠, 我是小春哥,从事 Java 后端开发,会一点前端、通过继续输入系列技术文章以文会友,如果本文能为您提供帮忙,欢送大家关注、点赞、分享反对,_咱们下期再见!_

正文完
 0