LongAdder 源码解析(基于 JDK 1.8)
LongAdder 和 LongAccumulator 基本一致,区别在于前者默认是加法,后者会同时传入一个表达式,具体结果是通过二元表达式计算得到的。
DoubleAdder 和 DoubleAccumulator 没有做什么,只是使用 Double.longBitsToDouble
和 Double.doubleToRawLongBits
进行 Long 和 Double 的转换,剩下的和前面两个类一样。
1 LongAdder
LongAdder 继承了 Striped64,可以实现高并发下的 Long 的计数,它和 AtomicLong 的区别在于:LongAdder 只保证最终一致性,也就是多个线程一起做加法,只保证都能把想写的数据写入,并不保证在中间的时刻都能看到,以及求和得到正确结果。
LongAdder 把数据保存在两个部分,base 和 数组 cells,如果并发量不大,直接写入 base;否则一部分结果会写入数组 cells。求和的时候只需要将 base 和 cells 的结果相加即可。这两个成员变量继承自 Striped64。
下面的方法中,最难理解的是 add,这里需要详细解释。
和上篇文章一样,仍然需要理解短路机制:对于逻辑运算符 a || b,如果 a 为 true,会直接返回 true,不会处理 b;否则,会处理 b,并返回 b 的结果。
-
首先判断 as 是否为空,如果不空,会进入下面的代码块;否则,说明 as 是空,即 as 还未初始化,尝试将结果写入,如果写入成功,那就不用继续操作了;如果写入失败,则进入下面的代码块
-
记 uncontended 为 true, uncontended 翻译为“无竞争”。
-
判断 as 是否为空;如果不空,检查是否长度为0;如果长度不是0,则获取 cells 的某一个位置,并判断是否初始化。
这三个判断如果有一个为 true,注意到 uncontended 一直是 true,调用 longAccumulate。
如果前三个都是 false,进入第四个 cas 的处理,如果写入成功,就结束了,不执行 longAccumulate;否则,说明写入失败,uncontended 为 false,调用 longAccumulate。
直观上的理解,有两次写入:先写入base;如果写入 base 失败,会尝试写入某一个 cell;如果还失败,就需要 longAccumulate。这个在后面再讲。
public class LongAdder extends Striped64 implements Serializable {
private static final long serialVersionUID = 7249069246863182397L;
/**
* Creates a new adder with initial sum of zero.
*/
public LongAdder() {
}
/**
* Adds the given value.
*
* @param x the value to add
*/
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 1. 判断 cells 和 进行 casBase 操作
// casBase 是 Striped64 的方法,用于修改 base 的值。
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
// getProbe 表示随机获取一个值
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);
}
}
/**
* Equivalent to {@code add(1)}.
*/
public void increment() {
add(1L);
}
/**
* Equivalent to {@code add(-1)}.
*/
public void decrement() {
add(-1L);
}
/**
* Returns the current sum. The returned value is <em>NOT</em> 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.
*
* @return the sum
*/
// 求和,如果 cells 里面有值就会加到 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;
}
/**
* Resets variables maintaining the sum to zero. This method may
* be a useful alternative to creating a new adder, but is only
* effective if there are no concurrent updates. Because this
* method is intrinsically racy, it should only be used when it is
* known that no threads are concurrently updating.
*/
// 将所有位置 base 和 cells 的每一个部分恢复为0
public void reset() {
Cell[] as = cells; Cell a;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
a.value = 0L;
}
}
}
/**
* Equivalent in effect to {@link #sum} followed by {@link
* #reset}. This method may apply for example during quiescent
* points between multithreaded computations. If there are
* updates concurrent with this method, the returned value is
* <em>not</em> guaranteed to be the final value occurring before
* the reset.
*
* @return the sum
*/
//一边求和,一边将对应位置恢复为0
public long sumThenReset() {
Cell[] as = cells; Cell a;
long sum = base;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null) {
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
...
}
2 Striped64
前面的 LongAdder,在 add 方法中使用了 longAccumulate,这个最重要的方法在 Striped64 中。
在讲这个方法之前,先介绍一下别的方法和变量。
@sun.misc.Contended 表示使用了缓存行填充。启动时必须加上参数生效 XX:-RestrictContended
每个 CPU 都有自己的缓存。缓存与主内存进行数据交换的基本单位叫Cache Line(缓存行)。在64位x86架构中,缓存行是64字节,也就是8个Long型的大小。这也意味着当缓存失效,要刷新到主内存的时候,最少要刷新64字节。
如果是图 1,则X 失效时,Y和Z 也会失效,这称为伪共享;为了将它们分开,可以将它们三个分别填充上 7 个 Long,这样就会每个变量占一个缓存行,如图2,防止了伪共享问题。
内部类 Cell,有一个 volatile long value,以及相应的 cas 方法。
对于 base,cellsbusy 和 probe,表示对应的值或者数组;大写的三个单词表示偏移量。
abstract class Striped64 extends Number {
/**
* Padded variant of AtomicLong supporting only raw accesses plus CAS.
*
* JVM intrinsics note: It would be possible to use a release-only
* form of CAS here, if it were provided.
*/
@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);
}
}
}
/** Number of CPUS, to place bound on table size */
// CPU 的个数,限制 cells 表的最大长度为:
//满足 (>=NCPU && 长度为2的幂) 的最小值
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Table of cells. When non-null, size is a power of 2.
*/
// 非空时,cells 要求长度为2的幂
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
// base
transient volatile long base;
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
// 在扩容或者写入的时候作为锁,平时为0,加锁时为1
transient volatile int cellsBusy;
/**
* Package-private default constructor
*/
Striped64() {
}
/**
* CASes the base field.
*/
// cas修改 base
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
/**
* CASes the cellsBusy field from 0 to 1 to acquire lock.
*/
// cas修改 cellbusy,将 0 变成 1,表示加锁
final boolean casCellsBusy() {
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
/**
* Returns the probe value for the current thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
/**
* Pseudo-randomly advances and records the given probe value for the
* given thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
// 做一个伪随机
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
...
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 注意,下面的三个大写的单词,表示偏移量
// 小写的单词表示对应的值或者数组
private static final long BASE;
private static final long CELLSBUSY;
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> sk = Striped64.class;
BASE = UNSAFE.objectFieldOffset
(sk.getDeclaredField("base"));
CELLSBUSY = UNSAFE.objectFieldOffset
(sk.getDeclaredField("cellsBusy"));
Class<?> tk = Thread.class;
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
} catch (Exception e) {
throw new Error(e);
}
}
}
下面在讲解时,同一级别的互斥关系 if/else if/else 用同一等级的序号表示,比如:1/2/3 ;1.1/1.2/1.3;对于同一级别但不具有互斥关系的if/else/try/catch,会重新使用前面的序号
具体操作如下:
首先判断 h 是否为0,如果为0,在下面执行 h & (n-1) 时只能得到0,所以重新初始化,并重新获取 h,将 wasUncontended 修改为 true。
记 collide 为 false,执行下面的 for 循环
1 (表不空的情况) 如果 as 非空,且长度大于0
1.1 如果 as 在计算出的位置 (n-1) & h 为空
1.1.1 如果 cellsBusy 为0,也就是没有上锁。新建一个值大小为 x 的cell
1.1.1.1 再次检查,如果 cellsBusy 不为空,且 casCellsBusy 执行成功。定义 created 为 false,尝试将这个 cell 放入。
1.1.1.1.1 执行 try 操作,在内部检查 cells 不空 && 长度大于 0 && 对应位置的 cell 为 null,则将对应的值放入,并修改 created 为 true;
1.1.1.1.2 在 finally 中,释放锁 cellsBusy。
下面的 if 和 try-catch 是并列关系,但不具备类似 if 和 else 的互斥关系,故重新从 1 开始排序
1.1.1.1.1 如果 created 为 true,表示新的 cell 放入成功,退出整个循环,结束。
1.1.1.1.2 (相当于写了 else)会跳过本次循环,执行下次 for 循环。
1.1 如果之前既没有 break,也没有 continue,会修改 collide 为false。
1.2 如果 wasUncontended 为 false,表示在 LongAdder 中使用 cas 修改 cell 失败,将其修改为 true。
1.3 执行 cas 操作,修改 cell 为 a 的值,分两种情况,如果是 LongAdder,传入的函数是 null,表示简单相加;如果是 LongAccumulator,会根据传入的表达值进行计算。如果 cas 成功,退出整个循环。
1.4 两种情况,如果 n >= NCPU,会将 collide 设置为 false(但由于长度 n 不会减小,该表达式一直成立,所以在接下来的循环中,要么在前面的判断语句中执行了,要么就再次进入 1.4,空转);如果是 cells != as,表示读到的数据 as 已经过期,此时也将 collide 设置为 false。
1.5 如果 collide 为 false,则修改为 true。
1.6 如果 cellsBusy 为0,执行 casCellsBusy 成功
1.6.1 执行 try,判断 cells 是否还是 as,是的话,将 cell 数组扩容,注意,新扩容出来的位置 rs[n]-rs[2*n-1] 没有初始化,节省时间,留待以后的线程执行 1.1.1.1.2 来创建。在 finally 中释放锁 cellsBusy。
1.6 修改 collide 为 false,跳过本次循环
1 执行 advanceProbe ,重新计算 h,使 (n - 1) & h 变成新的位置。类似于别的类中的 rehash。
2 (初始化) 如果 cellsBusy 为0,且 cells == as,cas 修改 busy 成功,定义 init 为 false
2.1 在 try 中,再次判断 cells == as,如果是,则新建一个长度为2的表,并将新的 Cell(x) 放入某个位置,设置 init 为 true
2.2 在 finally 中,释放锁 cellsBusy
下面的 if 和 try-catch 是并列关系,但不具备类似 if 和 else 的互斥关系,故重新从 1 开始排序
2.1 如果 init 为 true,说明写入成功,退出整个循环
3 (写入 base) 尝试直接写入到 base 中,写入成功同样退出循环。
还有几个问题需要解释一下
-
cellsBusy 为0表示未加锁,为1表示加锁,无论扩容还是新建,都需要加锁和解锁。
-
collide 意思为“碰撞”,一般为 false 时表示出现碰撞。
-
如果 1.2-1.5 为 false,并不是空转,实际上在这一行
h = advanceProbe(h)
,修改了h,使得后面再次循环处理时写入的位置会发生变化,避开冲突位置。 -
双重检验:在代码中,反复出现内外两个 if 判断
cellsBusy == 0
(如 1.1.1 和1.1.1.1) 以及cells ==as
(如 2 和2.1 的 if),这称为双重检验。由于在多线程中,代码执行可能会被打断,在计入内部循环前,可能锁状态或者 cells 数组已经发生变化,所以需要再次检查。这方面著名的例子是单例模式的懒汉式的双重检验。 -
cellsBusy == 0 && casCellsBusy()
并不多余,前面的操作快,后面的慢,之所以执行前面的判断是为了尽量减少执行慢的 cas 操作。
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;
// 1 cells不空
if ((as = cells) != null && (n = as.length) > 0) {
// 1.1 as 对应位置的 cell 不空
if ((a = as[(n - 1) & h]) == null) {
// 1.1.1 还没有加锁
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
// 1.1.1.1 再次检查是否加锁,并尝试加锁
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
// 1.1.1.1.1 尝试放入新的 cell
try { // Recheck under lock
Cell[] rs; int m, j;
// 如果表不空,且当前 cell 为空
// 尝试放入
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
//1.1.1.1.2 解锁
} finally {
cellsBusy = 0;
}
//新的 1.1.1.1.1 ,表示已经成功放入 r,可以退出
if (created)
break;
//省略了else,1.1.1.1.2 ,否则,没有成功,继续下次循环
continue; // Slot is now non-empty
}
}
// 同样是 1.1
collide = false;
}
// 1.2 表示 LongAdder 或者 LongAccumulator 在 cas 写入 cell 失败
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 1.3 尝试写入 a 中
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 1.4 如果 n>=NCPU,修改collide,且无法到下面扩容分支
// 如果数据变旧即 cells != as 修改 collide
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 1.5 如果 collide 为false,设置为 true
else if (!collide)
collide = true;
// 1.6 如果没加锁尝试加锁
else if (cellsBusy == 0 && casCellsBusy()) {
// 1.6.1 // 如果数据没有变旧,尝试扩容
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
}
// 这个属于第一个if,即 1 ,重新计算 h
h = advanceProbe(h);
}
// 2 如果没加锁且 as 是最新数据,尝试加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
// 2.1 再次检查 as 是否是最新数据
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
//2.2 释放锁
} finally {
cellsBusy = 0;
}
// 重新排列后的2.1,如果 init,说明新建成功
if (init)
break;
}
// 3 尝试直接写入 base,如果成功则退出
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
下面是我的公众号,Java与大数据进阶,分享 Java 与大数据笔面试干货,欢迎关注