一. 性能对比
阿里开发手册推荐jdk8使用LongAdder替代AtomicLong
示例代码
题目:热点商品点赞计算器,点赞数加加统计,不要求实时精确。50个线程,每个线程100W次,统计总点赞数
比较synchronized、AtomicInteger、AtomicLong、LongAdder、LongAccumulator五种计数性能
class ClickNumber{ int number = 0; public synchronized void add_synchronized(){ number++; } AtomicInteger atomicInteger = new AtomicInteger(); public void add_AtomicInteger(){ atomicInteger.incrementAndGet(); } AtomicLong atomicLong = new AtomicLong(); public void add_AtomicLong(){ atomicLong.incrementAndGet(); } LongAdder longAdder = new LongAdder(); public void add_LongAdder(){ longAdder.increment(); } LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0); public void add_longAccumulator(){ longAccumulator.accumulate(1); } } public class LongAdderCalcDemo { public static final int SIZE_THREAD = 50; public static final int _1w = 10000; public static void main(String[] args) throws InterruptedException { ClickNumber clickNumber = new ClickNumber(); long startTime = System.currentTimeMillis(); long endTime = System.currentTimeMillis(); CountDownLatch latch_synchronized = new CountDownLatch(SIZE_THREAD); CountDownLatch latch_AtomicInteger= new CountDownLatch(SIZE_THREAD); CountDownLatch latch_AtomicLong = new CountDownLatch(SIZE_THREAD); CountDownLatch latch_LongAdder = new CountDownLatch(SIZE_THREAD); CountDownLatch latch_LongAccumulator = new CountDownLatch(SIZE_THREAD); startTime = System.currentTimeMillis(); for (int i = 1; i <= SIZE_THREAD; i++) { new Thread(()->{ try { for (int j = 1; j <= 100*_1w; j++) { clickNumber.add_synchronized(); } } catch (Exception e) { e.printStackTrace(); } finally { latch_synchronized.countDown(); } },String.valueOf(i)).start(); } latch_synchronized.await(); endTime = System.currentTimeMillis(); System.out.println("synchronized花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.number); startTime = System.currentTimeMillis(); for (int i = 1; i <= SIZE_THREAD; i++) { new Thread(()->{ try { for (int j = 1; j <= 100*_1w; j++) { clickNumber.add_AtomicInteger(); } } catch (Exception e) { e.printStackTrace(); } finally { latch_AtomicInteger.countDown(); } },String.valueOf(i)).start(); } latch_AtomicInteger.await(); endTime = System.currentTimeMillis(); System.out.println("AtomicInteger花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.atomicInteger.get()); startTime = System.currentTimeMillis(); for (int i = 1; i <= SIZE_THREAD; i++) { new Thread(()->{ try { for (int j = 1; j <= 100*_1w; j++) { clickNumber.add_AtomicLong(); } } catch (Exception e) { e.printStackTrace(); } finally { latch_AtomicLong.countDown(); } },String.valueOf(i)).start(); } latch_AtomicLong.await(); endTime = System.currentTimeMillis(); System.out.println("AtomicLong花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.atomicLong.get()); startTime = System.currentTimeMillis(); for (int i = 1; i <= SIZE_THREAD; i++) { new Thread(()->{ try { for (int j = 1; j <= 100*_1w; j++) { clickNumber.add_LongAdder(); } } catch (Exception e) { e.printStackTrace(); } finally { latch_LongAdder.countDown(); } },String.valueOf(i)).start(); } latch_LongAdder.await(); endTime = System.currentTimeMillis(); System.out.println("LongAdder花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.longAdder.longValue()); startTime = System.currentTimeMillis(); for (int i = 1; i <= SIZE_THREAD; i++) { new Thread(()->{ try { for (int j = 1; j <= 100*_1w; j++) { clickNumber.add_longAccumulator(); } } catch (Exception e) { e.printStackTrace(); } finally { latch_LongAccumulator.countDown(); } },String.valueOf(i)).start(); } latch_LongAccumulator.await(); endTime = System.currentTimeMillis(); System.out.println("LongAccumulator花费时间:"+ (endTime-startTime)+" 数值为:"+clickNumber.longAccumulator.longValue()); } }
通过结果,可知LongAdder性能最优,花费时间最短,远优于AtomicLong
二. LongAdder为何那么快
LongAdder的基本思路
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。sum()会将所有Cell数组中的value和base累加作为返回值,
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
LongAdder的原理
LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作(无并发,单线程下直接CAS操作更新base值;非竞态条件下,直接累加到变量base上)
当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组Cells.多个线程需要同时对value进行操作时,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。(有并发,多线程下分段CAS操作更新Cell数组值;竞态条件下,累加个各个线程自己的槽Cell[]中)
sum()公式
三. 源码解析
1. LongAdder.add()
Cell[] as; long b, v; int m; Cell a;
as是striped64中的cells数组属性
b是striped64 中的base属性 v是当前线程hash到的Cell中存储的值
m是cells的长度减1,hash时作为掩码使用
a是当前线程hash到的Cell
if ((as = cells) != null || !casBase(b = base, b + x))
条件1: cells不为空,说明出现过竞争,Ccell[]已创建
条件2: cas操作base失败,说明其它线程先一步修改 了base正在出现竞争
首次首线程((as = cells) != null)一定是false,此时走casBase方法,以CAS的方式更新base值,且只有当cas失败时,才会走到if中
boolean uncontended = true
true无竞争,false表示竞争激烈,多个线程hash到同一个cell,可能要扩容
if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))
条件1: cells为空,说明正在出现竞争,上面是从条件2过来的
条件2: 应该不会出现
条件3: 当前线程所在的cell为空,说明当前线程还没有更新过cell,应初始化一个cell
条件4: 更新当前线程所在的cell失败,说明现在竞争很激烈,多个线程hash到了同一个个Cell,应扩容
longAccumulate(x, null, uncontended)
调用striped64中的方法处理
小结
- 1.最初无竞争时只更新base;
- 2.如果更新base失败后,首次新建一个Cell[]数组
- 3.当多个线程竞争同一个CelI比较激烈时,可能就要对Cell[]扩容
2. Striped64.longAccumulate()
longAccumulate()方法的入参
long X需要增加的值,一般默认都是1
LongBinaryOperator fn默认传递的是null
wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化之后, 并且当前
失败,才会是false
代码解释
上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
CASE1: Cell[]数组已经初始化
CASE2: Cell[]数组未初始化(首次新建)
如果上面条件都执行成功就会执行数组的初始化及赋值操作
Cell[] rs = new Cell[2]表示数组的长度为2;rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1;h & 1类似于HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1),同hashmap一个意思
CASE3: Cell[]数组正在初始化中
多个线程尝试CAS修改失败的线程会走到这个分支,该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化,多个线程正在更新base的值。
3. LongAdder.sum()
sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的(也就是说并发情况下,sum的值并不精确)
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果
四. 与AtomicLong对比
AtomicLong
- 原理
CAS + 自旋
-
场景
低并发下的全局计算,AlomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。可允许一些性能损耗,要求高精度时可使用。AtomicLong是多个线程针对单个热点值value进行原子操作
-
缺陷
高并发后性能急剧下降。(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,占用大量CPU)
LongAdder
- 原理
CAS+Base+Cell数组分散,通过空间换时间分散了热点数据
- 场景
高并发下的全局计算,当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用。LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
- 缺陷
sum求和后还有计算线程修改结果的话,最后结果不够准确
原文链接:https://blog.csdn.net/weixin_43899792/article/details/124575032