突击并发编程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
又提供了LongAdder
、LongAccumulator
;针对Double
类型,Java
提供了DoubleAdder
、DoubleAccumulator
。Striped64
相干的类的继承档次如下所示。
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 后端开发,会一点前端、通过继续输入系列技术文章以文会友,如果本文能为您提供帮忙,欢送大家关注、 点赞、分享反对,_咱们下期再见!_