[toc]
AtomicLong内部是一个volatile long型变量,由多个线程对这个变量进行CAS操作。多个线程同时对一个变量进行CAS操作,在高并发的场景下仍不够快,如果再要提高性能,该怎么做呢?把一个变量拆成多份,变为多个变量,有些类似于ConcurrentHashMap 的分段锁的例子。
把一个Long型拆成一个base变量外加多个Cell,每个Cell包装了一个Long型变量。当多个线程并发累加的时候,如果并发度低,就直接加到base变量上;如果并发度高,冲突大,平摊到这些Cell上。在最后取值的时候,再把base和这些Cell求sum运算。
Striped64类
1 | package java.util.concurrent.atomic; |
longAccumulate()方法
longAccumulate会根据当前线程来计算一个哈希值,然后根据算法(hashCode & (length - 1))来达到取模的效果以定位到该线程被分散到的Cell数组中的位置
如果Cell数组还没有被创建,那么就去获取cellBusy这个共享变量(相当于锁,但是更为轻量级),如果获取成功,则初始化Cell数组,初始容量为2,初始化完成之后将x保证成一个Cell,哈希计算之后分散到相应的index上。如果获取cellBusy失败,那么会试图将x累计到base上,更新失败会重新尝试直到成功。
如果Cell数组以及被初始化过了,那么就根据线程的哈希值分散到一个Cell数组元素上,获取这个位置上的Cell并且赋值给变量a,这个a很重要,如果a为null,说明该位置还没有被初始化,那么就初始化,当然在初始化之前需要竞争cellBusy变量。
如果Cell数组的大小已经最大了(CPU的数量),那么就需要重新计算哈希,来重新分散当前线程到另外一个Cell位置上再走一遍该方法的逻辑,否则就需要对Cell数组进行扩容,然后将原来的计数内容迁移过去。这里面需要注意的是,因为Cell里面保存的是计数值,所以在扩容之后没有必要做其他的处理,直接根据index将旧的Cell数组内容直接复制到新的Cell数组中就可以了。
1 | final void longAccumulate(long x, LongBinaryOperator fn, |
注意: Cell[]数组的大小始终是2的整数次方,在运行中会不断扩容,每次扩容都是增长2倍。
1 | /** |
LongAdder类
在LongAdder开篇的注释中,把它和AtomicLong 的使用场景做了比较。它适合高并发的统计场景,而不适合要对某个Long 型变量进行严格同步的场景。
1 | package java.util.concurrent.atomic; |
当一个线程调用add(x)的时候,首先会尝试使用casBase把x加到base变量上。如果不成功,则再用a.cas(..)函数尝试把x 加到Cell数组的某个元素上。如果还不成功,最后再调用longAccumulate(..)函数。
1 | public void add(long x) { |
LongAccumulator类
LongAdder只能进行累加操作,并且初始值默认为0;LongAccumulator可以自己定义一个二元操作符,并且可以传入一个初始值。
自定义二元操作符的接口
1 |
|
1 | public class LongAccumulator extends Striped64 implements Serializable { |
参考文献
作者:一字马胡
链接:https://www.jianshu.com/p/30d328e9353b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。