LongAdder是JDK1.8在java.util.concurrent.atomic包下新引入的 为了高并发下实现高性能统计的类。

1.背景

AtomicLong是在高并发下对单一变量进行CAS操作,从而保证其原子性。

public final long getAndAdd(long delta) {
return unsafe.getAndAddLong(this, valueOffset, delta);
}

在Unsafe类中,如果有多个线程进入,只有一个线程能成功CAS,其他线程都失败。失败的线程会重复进行下一轮的CAS,但是下一轮还是只有一个线程成功。

public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v= this.getLongVolatile(o,offset);
} while(!this.compareAndSwapLong(o,offset, v, v+delta));
return v;
}

即在高并发下,AtomicLong的性能会越来越差劲。

因此,引入了替代方案,LongAdder。

2.LongAdder

LongAdder是一种以空间换时间的解决方案。其内部维护了一个值base,和一个cell数组,当线程写base有冲突时,将其写入数组的一个cell中。将base和所有cell中的值求和就得到最终LongAdder的值了。

Method sum() (or, equivalently, longValue()) returns the current total combined across the variables maintaining the sum.

public long longValue() {
return sum();
} public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}

3.Striped64内部结构

LongAdder类继承了Striped64类,其中,class Striped64维护有有 Cell的内部类,Base,Cell数组等相关成员变量。

NCPU:表示当前计算机CPU数量,用于控制cells数组长度。因为一个CPU同一时间只能执行一个线程,如果cells数组长度 大于 CPU数量,并不能提高并发数,且造成空间的浪费。

cells:存放Cell的数组。

base:在没有发生过竞争时,数据会累加到base上。 或者,当cells扩容时,是需要将数据写到base中的。

cellsBusy:锁。0表示无锁状态,1表示其他线程已经持有锁。初始化cells,创建Cell,扩容cells都需要获取锁。

@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
} // Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset; // 当前value基于当前对象的内存偏移
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
//表示当前计算机CPU数量,控制cells数组长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
transient volatile Cell[] cells;
transient volatile long base; //在没有发生过竞争时,数据会累加到base上, 或者 当cells扩容时,需要将数据写到base中
transient volatile int cellsBusy; // 初始化cells或者扩容cells都需要获取锁,0表示无锁状态,1表示其他线程已经持有锁

4.LongAdder的add方法解析

add(long x):加上给定的x。

1.一开始只加给base,那么此时cells一定没有初始化,此时只会casBase,成功则返回。

2.casBase失败,意味着多线程写base发生竞争,进入longAccumulate(x, null, uncontended = true)重试或者初始化cells。

3.如果cells已经初始化过了,但是,当前线程对应下标的cell为空,需要创建。进入longAccumulate(x, null, uncontended = true)创建对应cell。

4.如果cells已经初始化过了,同时,当前线程对应的cell 不为空,cas给当前cell赋值,成功则返回。失败,意味着当前线程对应的cell 有竞争,进入longAccumulate(x, null, uncontended = false) 重试或者扩容cells。

    public void add(long x) {
//as 表示cells引用
//b 表示获取的base值
//v 表示 期望值
//m 表示 cells 数组的长度
//a 表示当前线程命中的cell单元格
Cell[] as; long b, v; int m; Cell a; //条件一:true->表示cells已经初始化过了,当前线程应该将数据写入到对应的cell中
// false->表示cells未初始化,当前所有线程应该将数据写到base中
//条件二:false->表示当前线程cas替换数据成功,
// true->表示发生竞争了,可能需要重试 或者 扩容
if ((as = cells) != null || !casBase(b = base, b + x)) {
//什么时候会进来?
//1.true->表示cells已经初始化过了,当前线程应该将数据写入到对应的cell中
//2.true->表示发生竞争了,可能需要重试 或者 扩容 boolean uncontended = true; //true -> 未竞争 false->发生竞争 //条件一:true->说明 cells 未初始化,也就是多线程写base发生竞争了
// false->说明 cells 已经初始化了,当前线程应该是 找自己的cell 写值
//条件二:getProbe() 获取当前线程的hash值 m表示cells长度-1 cells长度 一定是2的次方数 15= b1111
// true-> 说明当前线程对应下标的cell为空,需要创建 longAccumulate 支持
// false-> 说明当前线程对应的cell 不为空,说明 下一步想要将x值 添加到cell中。
//条件三:true->表示cas失败,意味着当前线程对应的cell 有竞争
// false->表示cas成功
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
//都有哪些情况会调用?
//1.true->说明 cells 未初始化,也就是多线程写base发生竞争了[重试|初始化cells]
//2.true-> 说明当前线程对应下标的cell为空,需要创建 longAccumulate 支持
//3.true->表示cas失败,意味着当前线程对应的cell 有竞争[重试|扩容]
longAccumulate(x, null, uncontended);
}
}

5.Striped64的longAccumulate方法解析

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended)

根据LongAdder的add方法可知,参数x是add函数的传入参数,即要增加的数;

LongBinaryOperator是一个接口可扩展,重写applyAsLong方法用于处理cell中值与参数x的关系,此处传null;

wasUncontended只有在 【cells已经初始化过了,同时,当前线程对应的cell 不为空,cas给当前cell赋值,竞争修改失败】的情况下为false,其他为true。

第一种情况:写base发生竞争,此时cells没有初始化,所以才会写到base,不走CASE1;

走Case2,判断有没有锁,没有锁的话,尝试加锁,成功加锁后执行初始化cells的逻辑。如果没有拿到锁,表示其它线程正在初始化cells,所以当前线程将值累加到base。

第二种情况:当前线程对应下标的cell为空,满足CASE1,到达CASE1.1中,创建一个Cell,加锁,如果成功,对应的位置其他线程没有设置过cell,将创建的cell插入相应位置。

第三种情况:当前线程对应下标的cell已经创建成功,但写入cell时发生竞争,到达CASE1.2,wasUncontended = true,把发生竞争线程的hash值rehash。

重置后走若CASE1.1,CASE1.2均不满足,到达CASE1.3【当前线程rehash过hash值,然后新命中的cell不为空】重试cas赋值+x一次,成功则退出。失败,扩容意向设置成true,rehash当前线程的hash值,再到1.3重试,还失败走CASE1.6扩容。

注意:CASE1.4要求cells数组长度不能超过cpu数量,因为一个CPU同一时间只能执行一个线程,如果cells数组长度 大于 CPU数量,并不能提高并发数,且造成空间的浪费。

    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
//h 表示线程hash值
int h;
//条件成立:说明当前线程 还未分配hash值; getProbe()获取当前线程的Hash值
if ((h = getProbe()) == 0) {
//给当前线程分配hash值
ThreadLocalRandom.current(); // force initialization
//取出当前线程的hash值 赋值给h
h = getProbe();
//为什么? 因为默认情况下 当前线程hash为0, 肯定是写入到了 cells[0] 位置。 不把它当做一次真正的竞争
wasUncontended = true;
} //表示扩容意向 false 一定不会扩容,true 可能会扩容。
boolean collide = false; // True if last slot nonempty //自旋
for (;;) {
//as 表示cells引用
//a 表示当前线程命中的cell
//n 表示cells数组长度
//v 表示 期望值
Cell[] as; Cell a; int n; long v; //CASE1: 表示cells已经初始化了,当前线程应该将数据写入到对应的cell中
if ((as = cells) != null && (n = as.length) > 0) {
// 以下两种情况会进入Case1:
//2.true-> 说明当前线程对应下标的cell为空,需要创建 longAccumulate 支持
//3.true->表示cas失败,意味着当前线程对应的cell 有竞争[重试|扩容] //CASE1.1:true->表示当前线程对应的下标位置的cell为null,需要创建new Cell
if ((a = as[(n - 1) & h]) == null) { //true->表示当前锁 未被占用 false->表示锁被占用
if (cellsBusy == 0) { // Try to attach new Cell //拿当前的x创建Cell
Cell r = new Cell(x); // Optimistically create //条件一:true->表示当前锁 未被占用 false->表示锁被占用
//条件二:true->表示当前线程获取锁成功 false->当前线程获取锁失败..
if (cellsBusy == 0 && casCellsBusy()) {
//是否创建成功 标记
boolean created = false;
try { // Recheck under lock
//rs 表示当前cells 引用
//m 表示cells长度
//j 表示当前线程命中的下标
Cell[] rs; int m, j; //条件一 条件二 恒成立
//rs[j = (m - 1) & h] == null 为了防止其它线程初始化过该位置,然后当前线程再次初始化该位置
//导致丢失数据
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
//扩容意向 强制改为了false
collide = false;
}
// CASE1.2:
// wasUncontended:只有cells初始化之后,并且当前线程 竞争修改失败,才会是false
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//CASE 1.3:当前线程rehash过hash值,然后新命中的cell不为空
//true -> 写成功,退出循环
//false -> 表示rehash之后命中的新的cell 也有竞争 重试1次 再重试1次
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
//CASE 1.4:
//条件一:n >= NCPU true->扩容意向 改为false,表示不扩容了 false-> 说明cells数组还可以扩容
//条件二:cells != as true->其它线程已经扩容过了,当前线程rehash之后重试即可
else if (n >= NCPU || cells != as)
//扩容意向 改为false,表示不扩容了
collide = false; // At max size or stale
//CASE 1.5:
//!collide = true 设置扩容意向 为true 但是不一定真的发生扩容
else if (!collide)
collide = true;
//CASE 1.6:真正扩容的逻辑
//条件一:cellsBusy == 0 true->表示当前无锁状态,当前线程可以去竞争这把锁
//条件二:casCellsBusy true->表示当前线程 获取锁 成功,可以执行扩容逻辑
// false->表示当前时刻有其它线程在做扩容相关的操作。
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//cells == as
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
//释放锁
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//重置当前线程Hash值
h = advanceProbe(h);
}
//CASE2:前置条件cells还未初始化 as 为null
//条件一:true 表示当前未加锁
//条件二:cells == as?因为其它线程可能会在你给as赋值之后修改了 cells
//条件三:true 表示获取锁成功 会把cellsBusy = 1,false 表示其它线程正在持有这把锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
//cells == as? 防止其它线程已经初始化了,当前线程再次初始化 导致丢失数据
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//CASE3:
//1.当前cellsBusy加锁状态,表示其它线程正在初始化cells,所以当前线程将值累加到base
//2.cells被其它线程初始化后,当前线程需要将数据累加到base
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}

6.总结

官方文档是这样介绍的

This class is usually preferable to AtomicLong when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

LongAdder在多个线程更新一个用于收集统计信息的而不是追求同步的公共和的情况下,是优于AtomicLong类的。在并发度小,低竞争情况下,两个类具有相似的性能。但是在高争用情况下,LongAdder的预期吞吐量要高得多,代价是更高的空间消耗。

最后,我们再来看一下sum方法的注释

Returns the current sum. The returned value is NOT an atomic snapshot; invocation in the absence of concurrent updates returns an accurate result, but concurrent updates that occur while the sum is being calculated might not be incorporated.

sum方法返回值只是一个接近值,并不是一个准确值。它在计算总和时,并发的更新并不会被合并在内。

总结:

  • LongAdder是一种以空间换时间的解决方案,其在高并发,竞争大的情况下性能更优。
  • 但是,sum方法拿到的只是接近值,追求最终一致性。如果业务场景追求高精度,高准确性,用AtomicLong。

【java学习笔记】LongAdder的更多相关文章

  1. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  2. 0035 Java学习笔记-注解

    什么是注解 注解可以看作类的第6大要素(成员变量.构造器.方法.代码块.内部类) 注解有点像修饰符,可以修饰一些程序要素:类.接口.变量.方法.局部变量等等 注解要和对应的配套工具(APT:Annot ...

  3. Java学习笔记(04)

    Java学习笔记(04) 如有不对或不足的地方,请给出建议,谢谢! 一.对象 面向对象的核心:找合适的对象做合适的事情 面向对象的编程思想:尽可能的用计算机语言来描述现实生活中的事物 面向对象:侧重于 ...

  4. 0032 Java学习笔记-类加载机制-初步

    JVM虚拟机 Java虚拟机有自己完善的硬件架构(处理器.堆栈.寄存器等)和指令系统 Java虚拟机是一种能运行Java bytecode的虚拟机 JVM并非专属于Java语言,只要生成的编译文件能匹 ...

  5. 0030 Java学习笔记-面向对象-垃圾回收、(强、软、弱、虚)引用

    垃圾回收特点 垃圾:程序运行过程中,会为对象.数组等分配内存,运行过程中或结束后,这些对象可能就没用了,没有变量再指向它们,这时候,它们就成了垃圾,等着垃圾回收程序的回收再利用 Java的垃圾回收机制 ...

  6. 0028 Java学习笔记-面向对象-Lambda表达式

    匿名内部类与Lambda表达式示例 下面代码来源于:0027 Java学习笔记-面向对象-(非静态.静态.局部.匿名)内部类 package testpack; public class Test1{ ...

  7. 0025 Java学习笔记-面向对象-final修饰符、不可变类

    final关键字可以用于何处 修饰类:该类不可被继承 修饰变量:该变量一经初始化就不能被重新赋值,即使该值跟初始化的值相同或者指向同一个对象,也不可以 类变量: 实例变量: 形参: 注意可以修饰形参 ...

  8. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  9. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  10. 0013 Java学习笔记-面向对象-static、静态变量、静态方法、静态块、单例类

    static可以修饰哪些成员 成员变量---可以修饰 构造方法---不可以 方法---可以修饰 初始化块---可以修饰 内部类(包括接口.枚举)---可以修饰 总的来说:静态成员不能访问非静态成员 静 ...

随机推荐

  1. Python File writelines() 方法

    概述 writelines() 方法用于向文件中写入一序列的字符串.高佣联盟 www.cgewang.com 这一序列字符串可以是由迭代对象产生的,如一个字符串列表. 换行需要制定换行符 \n. 语法 ...

  2. 实验04——java保留小数的两种方法、字符串转数值

    package cn.tedu.demo; import java.text.DecimalFormat; /** * @author 赵瑞鑫 E-mail:1922250303@qq.com * @ ...

  3. markdown公式指导手册

    #Cmd Markdown 公式指导手册 标签: Tutorial 转载于https://www.zybuluo.com/codeep/note/163962#1%E5%A6%82%E4%BD%95% ...

  4. 《RabbitMQ》如何保证消息不被重复消费

    一 重复消息 为什么会出现消息重复?消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复. 1.1 生产时消息重复 由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认 ...

  5. Jenkins持续集成(上)-Windows下安装Jenkins

    环境:Windows 2008 R2.Jenkins2.235.1: 概要 前面写过一篇文章,<自动发布-asp.net自动发布.IIS站点自动发布(集成SLB.配置管理.Jenkins)> ...

  6. 03-java实现循环链表

    03java实现循环链表 本人git https://github.com/bigeyes-debug/Algorithm 一丶单向循环链表 就是为尾节点指向头结点 二丶单向循环链表的接口设计 比较单 ...

  7. 关于Springboot配置文件的理解

    一.Springboot Springboot是用来简化Spring框架搭建和开发一款框架,可以理解为是一种Spring框架的简化版. 二.如何在IDEA里面初始化Springboot 主要可以分为两 ...

  8. salesforce零基础学习(九十九)Git 在salesforce项目中的应用(vs code篇)

    本篇参考: https://code.visualstudio.com/docs/editor/versioncontrol https://git-scm.com/doc https://git-s ...

  9. Django-model查询[为空、由某字符串开头、由某字符串结尾、包含某字符串],__isnull、__starswith、__endswith、__contains

    使用属性+__isnull就可以判断此字段为空 a = DatasClass.objects.filter(name__isnull=True) 使用属性+__startswith可以判断属性由某字符 ...

  10. 2020-07-28:已知sqrt (2)约等于 1.414,要求不用数学库,求sqrt (2)精确到小数点后 10 位。

    福哥答案2020-07-28: 1.二分法.2.手算法.3.牛顿迭代法.基础是泰勒级数展开法.4.泰勒级数法.5.平方根倒数速算法,卡马克反转.基础是牛顿迭代法. golang代码如下: packag ...