本文已参与「新人创作礼」活动,一起开启掘金创作之路
1、Java中,锁占了并发的一席之地,但是锁带来的弊端就是线程会频繁的阻塞挂起,导致上下文的切换和重新调度,增加了系统开销。CAS 即 Compare and Swap(Change),是 JDK 提供的非阻塞原子性操作 , 它通过硬件保证了比较更新操作的原子性 ,有效减小了因为上下文切换导致的开销问题。
CAS底层使用的是lock cmpxchg指令,在单核CPU和多核CPU下都能保证【比较-交换】的原子性。在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制锁打断,保证了多个线程内部操作的准确性 和原子性。CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果。
2、AtomicLong都是java并发包JUC下面的类,其底层还是通过cas来保证原子性,因为cas底层通过CPU并发原语lock cmpxchg来保证的,但是这体现到一个缺点,当多个线程都对这个一个变量进行操作,会造成竞争激烈,重试必然频繁发生,反而效率会降低。这时候JDK1.8引入了LongAdder,这个是Doug Lea在1.8对CAS进行的优化。
3、LongAdder核心思想:将热点数据分离。
它可以将AtomicLong内部的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,而最终的计数结果则为这个cell数组的求和累加,其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值。当前对象的实际值由所有的cell累计合成,这样热点就进行了有效地分离,并提高了并行度。这相当于将AtomicLong的单点的更新压力分担到各个节点上。在低并发的时候通过对base的直接更新,可以保障和AtomicLong的性能基本一致。而在高并发的时候通过分散提高了性能。
底层通过cell累加单元,一个cell24字节(16个对象头8字节的value),一个缓存行一般是64个字节8个long,因为cell是数组,一个缓存行可以包括两个cell单元,修改一个cell,另一个cell也会失效,所以加上一个**@sun.misc.comtended注解主要是防止缓存行伪对齐**,他对齐的原理就在一个cell前后加上一个padding128字节,这样让每一个cell占用独立的缓存行,这样修改一个cell就不会影响其他的cell。
为什么有了AtomicLong还要新增一个LongAdder呢?
原因是:CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。(不停的自旋,进入一个无限重复的循环中)
LongAdder缺点:如果在统计的时候,如果有并发更新,可能会有统计数据有误差。实际使用中在处理高并发计数的时候优先使用LongAdder,而不是AtomicLong,在线程竞争不激烈的时候,使用AtomicLong会简单效率更高一些。比如序列号生成(准确性)
举个例子:
在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,比如刚开始累加成了base = 5(累加到一定数值)。并发更新的线程数量过多时,施行分段CAS的机制(Cell数组,每个数组是一个数值分段)这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了,这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!而且他内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。
这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。