简介
LongAddr是java8中新增的原子类,在竞争比较激烈的情况下性能要高出AtomicLong很多。
原理
LongAddr在最初无竞争的情况下会把值更新到base属性上,当存在竞争会初始化cells对象,把各个线程的值分段到不同的对象上,也类似于热点分段。
源码分析
LongAdder继承自Striped64抽象类,Striped64中定义了Cell内部类和各重要属性。
主要内部类
/**
* 消除伪共享的内部类
*
*/
@sun.misc.Contended static final class Cell {
volatile long value; // volatile修饰,修改之后对其它线程立即可见
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); // 原子更新value的值
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE; // 魔法类,对这个类不熟悉的可以自行学习
private static final long valueOffset; // value在Cell对象中的偏移量,一旦类被加载,对应属性的偏移量就固定了
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe(); // 获取魔法类,app类加载器加载的类不能通过这种方式使用
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset // 偏移地址
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
对于伪共享不太了解的也可以看这里:juejin.cn/post/694026…
主要属性
/**
* cells数组,存储热点对象Cell,在没有竞争的情况下是null
* 一旦存在竞争就会初始化cells数组,初始容量为2,最大不超过cpu的数量
* cells一旦初始化,就算后面没有竞争,线程更新值也是更新到Cell对象上,不会在更新到base属性上
*/
transient volatile Cell[] cells;
// 在没有竞争的情况下,值会更新此属性上
transient volatile long base;
// 乐观锁,当修改cells数组会使用此变量
transient volatile int cellsBusy;
add(long x)
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 1. 判断cells有没有初始化,如果base没有竞争,cells不会初始化
// 2. 如果cells是null且通过cas更新base成功,直接返回,否则有竞争,进入if语句内,初始化cells
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true; // 没有竞争
/**
* 1. as是null,成立则进入if语句块,执行初始化cells操作
* 2. as的数组长度是0
* 3. 如果cells被初始化,且它的长度不为0,则通过getProbe方法获取当前线程threadLocalRandomProbe变量的值,初始为0,
* 然后执行threadLocalRandomProbe&(cells.length-1 ),
* 相当于m%cells.length;如果cells[threadLocalRandomProbe%cells.length]的位置为null,
* 这说明这个位置从来没有线程做过累加,需要进入if继续执行,在这个位置创建一个新的Cell对象
* 4. 当前线程在cells能够匹配到cell对象,并且做过累加操作,如果cas更新失败,说明此cell存在竞争,uncontended为false
*/
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); // 调用父类方法,第一个参数是当前累加的值,第二个参数是当前cell中否存在竞争
}
}
- 不存在竞争者的情况下值只更新到base属性上
- 一旦有竞争就会初始化cells对象
- 如果当前线程hash到位置有竞争,就可能需要扩容了
longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended)
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) { // 获取当前线程的threadLocalRandomProbe作为hash,如果是0证明当前线程还没有竞争过cell
// 初始化当前线程的threadLocalRandomProbe
ThreadLocalRandom.current(); // force initialization
h = getProbe(); // 重新获取threadLocalRandomProbe
wasUncontended = true; // 不存在竞争
}
// collide 翻译过来的意思是否有碰撞,也可以理解为是否有扩容的意向,true是有意向扩容
boolean collide = false; // True if last slot nonempty
/**
* 先大概描述下for循环内所做的工作:
* if ((as = cells) != null && (n = as.length) > 0) {}
* else if (cellsBusy == 0 && cells == as && casCellsBusy()) {}
* else if (casBase(v = base, ((fn == null) ? v + x :
* fn.applyAsLong(v, x))))
最外层有三个判断条件
条件1. cells已经初始化且长度大于0,如果不成立说明cells还没有初始化,执行条件2进行初始化工作
条件2. cellsBusy没有被占用,cells是null,且cas cellsBusy成功,如果有一个失败说明有其它线程正在初始化cells,执行条件3
条件3. 有其它线程正在初始化cells,先把数值累加到base属性上
整体来看结构是非常清晰的,下面进入具体的分支分析。
*/
for (;;) { // 自旋
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) { // cells已经初始化
/**
* 内部分支1:当前线程hash到的位置内容为null,说明没有竞争,创建对应位置的cell对象
*/
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // 没有加锁 // Try to attach new Cell
Cell r = new Cell(x); // 创建Cell // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) { // 再次判断cellsBusy,且如果加锁成功执行以下逻辑
boolean created = false; // 创建Cell的状态
try { // Recheck under lock
Cell[] rs; int m, j;
// 再次判断cells不为null且长度大于0 且hash到位置的Cell为null
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r; // 把Cell放到hash到位置上
created = true; // 创建成功
}
} finally {
cellsBusy = 0; // 释放锁
}
if (created)
break;
continue; // 执行到这里说明获取锁之后有其它线程已经hash到对应位置上了,存储竞争,重新循环 // Slot is now non-empty
}
}
collide = false; // 没有扩容意向
}
/**
* 内部分支2:!wasUncontended 存在竞争,执行这个分支说明在子类尝试做过累加操作,但失败了
* wasUncontended = true; // 在这里改成不存在竞争,下次循环在尝试cas一次
*/
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
/**
* 内部分支3:cas做累加操作,如果累加成功则退出,累加失败说明当前Cell存在竞争,
* for循环的最低处会重新计算当前线程的threadLocalRandomProbe
*/
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
/**
* 内部分支4:如果Cell对象的个数大于等于cpu个数或者其它线程已经做过扩容
* collide = false; 扩容意向改成false
* 在这里特别说明一下:如果cells的容量已经达到了cpu的个数,下面两个内部分支将永远不会执行了,也就是不会在扩容了
* 只能每次重新计算下当前线程的threadLocalRandomProbe(有可能新hash到位置就是null了或者hash到Cell对象竞争不激烈了,在内部分支3 cas成功了就退出了)
*/
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
/**
* 内部分支5: 执行到这里如果collide=false,说明需要扩容了
* collide = true; 扩容意向改成有扩容意向,下次循环就要开始扩容了
*/
else if (!collide)
collide = true;
/**
* 内部分支6:如果没有其它线程加锁且cas加锁成功
*/
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // 检查cells有没有变化,有可能在加锁成功之后其它线程已经改了cells
Cell[] rs = new Cell[n << 1]; // 扩容为原来的2倍
for (int i = 0; i < n; ++i)
rs[i] = as[i]; // 对应位置的cell转移到新的数组上
cells = rs; // cells指向新的数组
}
} finally {
cellsBusy = 0; // 释放锁
}
collide = false; // 没有扩容意向
continue; // 开始下次循环
}
h = advanceProbe(h); // 重新计算线程的threadLocalRandomProbe
}
/**
* 外部分支2:cells数组还没有初始化, 如果cellsBusy是0且cells没有改变且cas加锁成功就准备初始化cells数组了
*/
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false; // 初始化数组状态
try { // Initialize table
if (cells == as) { // 再次判断cells指向有没有变化,有可能在当前线程cas锁成功之后,其它线程刚初始化了cells数组并释放了锁
Cell[] rs = new Cell[2]; // 初始化容量为2
rs[h & 1] = new Cell(x); // 占用一个位置
cells = rs; // cells指向新创建的数组对象
init = true; // 标识创建数组成功
}
} finally {
cellsBusy = 0; // 释放锁
}
if (init)
break; // 退出循环
}
/**
* 外部分支3:如果cells数组没有初始化且其它线程正在初始化,则当先线程的值先累加到base属性上
*/
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
-
cells已经初始化且长度大于0
1.1. 当前线程hash到的位置没有Cell对象,则尝试在此位置上创建Cell对象,成功放入此位置就返回,否则重试
1.2. 当前线程对应的Cell对象存在竞争
1.3. 尝试把值累加到对应的Cell对象上
1.4. 如果cells的容量大于cpu个数或者有其它线程已经扩容了
1.5. 有扩容意向
1.6. 扩用cells数组为之前的2倍 -
cells还未初始化且没有其它线程在初始化
-
cells正在初始化(其它线程)就把当前的值更新到base属性上
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;
}
sum方法相对来说还是比较简单的,就不多做介绍了,简单说一下:
- 如果cells不为null,说明cells数组已经初始化了,需要把Cell和base累加
- 如果调用sum的同时,有其它线程在操作某个段,有可能值不是最新的 参考: www.cnblogs.com/tong-yuan/p…