突击并发编程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 个并发线程测试

 // 并发线程数 10count=10000000耗时:305// 并发线程数 100count=100000000耗时:2301// 并发线程数 500count=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 个并发线程测试

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

总结

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


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