前言

在介绍 AtomicInteger 时,已经说明在高并发下大量线程去竞争更新同一个原子变量时,因为只有一个线程能够更新成功,其他的线程在竞争失败后,只能一直循环,不断的进行 CAS 尝试,从而浪费了 CPU 资源。而在 JDK 8 中新增了 LongAdder 用来解决高并发下变量的原子操作。下面同样通过阅读源码来了解 LongAdder 。

公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!

介绍

一个或多个变量共同维持初值为 0 总和。 当跨线程竞争更新时,变量集可以动态增长以减少竞争。 方法 sum 返回当前变量集的总和。

当多个线程更新时,这个类是通常优选 AtomicLong ,比如用于收集统计信息,不用于细粒度同步控制的共同总和。 在低更新竞争,这两个类具有相似的特征。 但在高更新竞争时,使用 LongAdder 性能要高于 AtomicLong,同样要消耗更高的空间为代价。

LongAdder 继承了 Striped64,内部维护一个 Cells 数组,相当于多个 Cell 变量, 每个 Cell 里面都有一个初始值为 0 的 long 型变量。

源码分析

Cell 类

Cell 类 是 Striped64 的静态内部类。

@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;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
  1. Cell 使用 @sun.misc.Contended 注解。
  2. 内部维护一个被 volatile 修饰的 long 型 value 。
  3. 提供 cas 方法,更新value。

其中 @sun.misc.Contended 注解作用是为了减少缓存争用。什么是缓存争用,这里只做下简要介绍。

伪共享

CPU 存在多级缓存,其中最小存储单元是 Cache Line,每个 Cache Line 能存储 64 个字节的数据。

在多线程场景下,A B 两个线程数据如果被存储到同一个 Cache Line 上,此时 A B 更新各自的数据,就会发生缓存争用,导致多个线程之间相互牵制,变成了串行程序,降低了并发。

@sun.misc.Contended 注解,则可以保证该变量独占一个 Cache Line。

详细可参考:http://openjdk.java.net/jeps/142

Striped64 核心属性

abstract class Striped64 extends Number {

    /** CPU 的数量,以限制表大小 */
static final int NCPU = Runtime.getRuntime().availableProcessors(); /**
* cell 数组,当非空时,大小是 2 的幂。
*/
transient volatile Cell[] cells; /**
* Base 值,在无争用时使用,表初始化竞赛期间的后备。使用 CAS 更新
*/
transient volatile long base; /**
* 调整大小和创建Cells时自旋锁(通过CAS锁定)使用。
*/
transient volatile int cellsBusy;
}

Striped64 类主要提供以下几个属性:

  1. NCPU:CPU 的数量,以限制表大小。
  2. cells:Cell[] cell 数组,当非空时,大小是 2 的幂。
  3. base:long 型,Base 值,在无争用时使用,表初始化竞赛期间的后备。使用 CAS 更新。
  4. cellsBusy:调整大小和创建Cells时自旋锁(通过CAS锁定)使用。

下面看是进入核心逻辑:

LongAdder#add

public class LongAdder extends Striped64 implements Serializable {

    public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// cells 是 数组,base 是基础值
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
}

abstract class Striped64 extends Number {
// 使用 CAS 更新 BASE 的值
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
// 返回当前线程的探测值。 由于包装限制,从ThreadLocalRandom复制
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
}

  1. 首先会对 Base 值进行 CAS 更新,当 Base 发生竞争时, 会更新数组内的 Cell 。
  2. 数组未初始化,Cell 未初始化, Cell 更新失败,即 Cell 也发生竞争时,会调用 Striped64 的 longAccumulate 方法。

Striped64#longAccumulate


abstract class Striped64 extends Number {
/**
* x 要增加的值
* wasUncontended 有没有发生竞争
*/
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
// 当前线程有无初始化线程探测值, 给当前线程生成一个 非 0 探测值
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
// 循环
for (;;) {
Cell[] as; Cell a; int n; long v;
// 数组不为空切数组长度大于 0
if ((as = cells) != null && (n = as.length) > 0) {
// (n - 1) & h 获取到索引,索引处 cell 是否为 null, cell未初始化
if ((a = as[(n - 1) & h]) == null) {
// 判断 cellsBusy 是否为 0
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
// cellsBusy == 0 且 使用 casCellsBusy 方法将其更新为 1,失败会继续循环
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 重新检查状态 并创建
if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
// 创建完成之后, 改回 cellsBusy 值
cellsBusy = 0;
}
if (created)
break;
// 未创建继续循环
continue; // Slot is now non-empty
}
}
collide = false;
}
// 传入的 wasUncontended 为 false 即发生碰撞了, 修改为未碰撞, 此处会继续循环,走到下一步,相当于会一直循环这个 cell
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// cas 更新 cell 的 value, 成功则返回
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 数组到最大长度 即大于等于 CPU 数量, 或者 cells 数组被改变,
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 乐观锁 进行扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
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
}
// 当前探针值不能操作成功,则重新设置一个进行尝试
h = advanceProbe(h);
}
// 没有加 cellsBusy 乐观锁 且 没有初始化,且获得锁成功(此时 cellsBusy == 1)
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 尝试在base上累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
}

longAccumulate 方法一共有三种情况

  1. (as = cells) != null && (n = as.length) > 0 数组不为空且长度大于 0 。

    1. 获取索引处的 cell , cell 为空则进行初始化。
    2. cell 不为空,使用 cas 更新, 成功 break; 跳出循环, 失败则还在循环内,会一直尝试。
    3. collide 指是否发生冲突,冲突后会进行重试。
    4. 冲突后会尝试获得锁并进行扩容,扩容长度为原来的 2 倍,然后继续重试。
    5. 获得锁失败(说明其他线程在扩容)会重新进行计算探针值。
  2. cellsBusy == 0 && cells == as && casCellsBusy() 数组为空,获得乐观锁成功。
    1. 直接初始化数组。
    2. 初始数组长度为 2 。
  3. casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))) 获得乐观锁失败。
    1. 说明有其他线程在初始化数组,直接 CAS 更新 base 。

LongAdder#sum


public class LongAdder extends Striped64 implements Serializable { 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;
}
}
  1. 数组为空,说明没有发生竞争,直接返回 base 。
  2. 数组不为空,说明发生竞争,累加 cell 的 value 和 base 的和进行返回。

总结

基本流程

  1. LongAdder 继承了 Striped64,内部维护一个 Cells 数组,相当于多个 Cell 变量, 每个 Cell 里面都有一个初始值为 0 的 long 型变量。
  2. 未发生竞争时(Cells 数组未初始化),是对 base 变量进行原子操作。
  3. 发生竞争时,每个线程对自己的 Cell 变量的 value 进行原子操作。

如何确定哪个线程操作哪个 cell?

通过 getProbe() 方法获取该线程的探测值,然后和数组长度 n - 1& 操作 (n - 1) & h 。

static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

Cells 数组初始化及扩容?

初始化扩容时会判断 cellsBusy, cellsBusy 使用 volatile 修饰,保证线程见可见性,同时使用 CAS 进行更新。 0 表示空闲,1 表示正在初始化或扩容。

初始化时会创建长度为 2 的 Cell 数组。扩容是创建一个长度是原数组长度 2 倍的新数组,并循环赋值。

如果线程访问分配的 Cell 元素有冲突后,会使用 advanceProbe() 方法重新获取探测值,再次进行尝试。

使用场景

在高并发情况下,需要相对高的性能,同时数据准确性要求不高,可以考虑使用 LongAdder。

当要保证线程安全,并允许一定的性能损耗时,并对数据准确性要求较高,优先使用 AtomicLong。

JDK 8 新增的 LongAdder,得过来看一下的更多相关文章

  1. JDK 7中的文件操作的新特性

    文件系统综述 一个文件系统在某种媒介(通常是一个或多个硬盘)上存储和组织文件.如今的大多数文件系统都是以树状结构来存储文件.在树的顶端是一个或多个根节点,在根节点一下,是文件和目录(在Windows系 ...

  2. Java JDK 版本的区别

    jdk6和jdk5相比的新特性有: 1.instrumentation 在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument.本地代码 ...

  3. JDK 9 & JDK 10 新特性

    JDK 9 新增了不少特性,官方文档:https://docs.oracle.com/javase/9/whatsnew/toc.htm#JSNEW-GUID-527735CF-44E1-4144-9 ...

  4. JDK 8 - Lambda Expression 的优点与限制

    我们知道 JDK 8 新增了 Lambda Expression 这一特性. JDK 8 为什么要新增这个特性呢? 这个特性给 JDK 8 带来了什么好处? 它可以做什么?不可以做什么? 在这篇文章, ...

  5. JDK 8 - JVM 对类的初始化探讨

    在<深入理解 Java 虚拟机>(第二版,周志明著)中,作者介绍了 JVM 必须初始化类(或接口)的五种情况,但是是针对 JDK 7 而言的. 那么,在 JDK 8 中,这几种情况有没有变 ...

  6. Java程序员必备基础:JDK 5-15都有哪些经典新特性

    前言 JDK 15发布啦~ 我们一起回顾JDK 5-15 的新特性吧,大家一起学习哈~ 本文已经收录到github ❝ https://github.com/whx123/JavaHome ❞ 「公众 ...

  7. 译文《全新首发JDK 16全部新特性》

    封面:洛小汐 译者:潘潘 JDK 8 的新特性都还没摸透,JDK 16 的新特性就提着刀来了. 郑重申明: 第一次冒险翻译专业领域的文献,可想而知,效果特别糟糕.一般翻译文献特别是 技术专业领域 的内 ...

  8. 【原创】JDK 9-17新功能30分钟详解-语法篇-var

    JDK 9-17新功能30分钟详解-语法篇-var 介绍 JDK 10 JDK 10新增了新的关键字--var,官方文档说作用是: Enhance the Java Language to exten ...

  9. 从 Linux 内核角度探秘 JDK NIO 文件读写本质

    1. 前言 笔者在 <从 Linux 内核角度看 IO 模型的演变>一文中曾对 Socket 文件在内核中的相关数据结构为大家做了详尽的阐述. 又在此基础之上介绍了针对 socket 文件 ...

随机推荐

  1. 深入解读 ASP.NET Core 身份认证过程

    长话短说:上文我们讲了 ASP.NET Core 基于声明的访问控制到底是什么鬼? 今天我们乘胜追击:聊一聊ASP.NET Core 中的身份验证. 身份验证是确定用户身份的过程. 授权是确定用户是否 ...

  2. This is Riv3r1and.

    总是要弄个博客来搞的嘛.

  3. 轻松理解JVM的分代模型

    前言 上篇文章我们一起对jvm的内存模型有了比较清晰的认识,小伙伴们可以参考JVM内存模型不再是秘密这篇文章做一个复习. 本篇文章我们将针对jvm堆内存的分代模型做一个详细的解析,和大家一起轻松理解j ...

  4. 云服务器、euleros系统自动断开连接解决方案

    我这里的云服务器,网上查的修改sshd.config文件并不有效 我提供另一种方法解决这个问题: vim /etc/profile 再最底部新增 export TMOUT=6000 #6000代表60 ...

  5. GIT之分支管理

    分支管理 一.分支推进 主分支 单线分支,随着代码的提交而形成的一条直线,HEAD 随着commit提交之后的节点移动而移动. 子分支 当切换到子分支的时候,HEAD 则指向子分支的节点. 在子分支上 ...

  6. day32 Pyhton 模块02复习 序列化

    一. 什么是序列化 在我们存储数据或者网络传输数据的时候. 需要对我们的对象进行处理. 把对象处理成方便存储和传输的数据格式. 这个过程叫序列化 不同的序列化, 结果也不同. 但是目的是一样的. 都是 ...

  7. 习题3-2 分子量(Molar Mass, ACM/ICPC Seoul 2007, UVa1586)

    #include<stdio.h> #include<string.h> #include<ctype.h> double getweight(char x) { ...

  8. 数据库SQL Server 2016“功能选择”详细说明及精简安装选择

    前言 在平时大家安装数据库的时候,一般默认功能选择都会选择全选.但是前两天公司同事问我:"那么多功能为什么都能用到嘛?"顿时,我思考了一下确实没有详细了解每个功能的详细作用,于是花 ...

  9. Vue中封装axios组件实例

    首先要创建一个网络模块network文件夹  里面要写封装好的几个组件 在config.js里面这样写 在index.js要这样写 core.js文件里面内容如下 然后要在main.js文件里面要设置 ...

  10. laravel 验证码 auth方式登录 中间件判断session是否存在

    首先下载laravel的插件 composer下载  实现验证码       composer require mews/captcha 在config/app.php进行配置 'providers' ...