共计 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
又提供了 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 个并发线程测试
// 并发线程数 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 后端开发,会一点前端、通过继续输入系列技术文章以文会友,如果本文能为您提供帮忙,欢送大家关注、点赞、分享反对,_咱们下期再见!_