继承结构
当我们进行并发统计的时候,为了避免线程安全问题,我们会使用AtomicInteger或者AtomicLong进行计数。一般我们会配合while循环一直进行cas直到累加成功。如果并发量很高的情况下,其内部的cas会一直自旋,性能并不高,因此Striped64诞生。Striped64是一个抽象类,支持高性能累加操作。先看看它的继承体系:
可以看到Striped64继承自Number,证明其是一个数字,其下有四个子类,分别是针对Long/Double的Adder/Accumulator,两两组合产生4个子类。这里先说下这里的高性能底层原理:首先存在一个base,并发不高的场合下将对base进行修改,如果并发高了之后,将会找到其下的Cell数组,根据当前线程进行hash找到对应的Cell进行修改,意味着其逻辑是将并发分拆到不同位置上进行操作。当需要获得总数的时候,将会 统计base与cell数组下的数据进行累计求和达到高性能的目的。
接下来我们先看看Striped64是怎么回事,然后挑选LongAdder与LongAccumulator进行分析。
Striped64
首先看看Striped64内所定义的关键字段:
// 通过分治的思想将对 base 的竞争分散到不同的 cell 单元中。
transient volatile Cell[] cells;
// 无竞争时直接更新 base
// 有竞争时同时更新 base 和 cells 数组
transient volatile long base;
// Spinlock 自旋锁
transient volatile int cellsBusy;
// Cell 对象封装了每个计算单元的值和常用方法。
@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;
// 获取 value 属性偏移量
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
首先存在一个叫做base的东西,没有竞争的时候直接更新base,当有竞争的场合下则更新Cell,可以看到每个Cell内部都存在一个value值,而多个Cell组成了数组。查看Cell的类定义可以发现,存在@sun.misc.Contended注解,这个注解帮助我们解决伪共享问题。所谓伪共享问题也很好理解,a,b位于同一个缓存行的场合下,针对a的更新会导致b同时失效,实际上b不需要失效,导致真正用到b的时候又得加载。如此反复性能自然就弱了。解决办法也很粗暴,就是在a边上挂上一些没什么用的对象,让他们独享一个缓存行。因此效率变高但是缓存占用量也会变多,算是用空间换时间的策略了。
getProbe
在冲突的时候需要定位到使用哪个Cell进行操作,这个时候就需要使用当前的线程求得其hash值
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
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);
}
}
如下代码所示,其值来自于Thread的threadLocalRandomProbe字段。类似在进入longAccumulate方法的的时候,threadLocalRandomProbe一直都是0,当发生争用后才会进入longAccumulate方法中,进入该方法第一件事就是判断threadLocalRandomProbe是否为0,如果为0,则将其设置为0x9e3779b9。probeGenerator 是static 类型的AtomicInteger类,每执行一次localInit()方法,都会将probeGenerator 累加一次0x9e3779b9这个值;,0x9e3779b9这个数字的得来是 2^32 除以一个常数,这个常数就是传说中的黄金比例 1.6180339887;然后将当前线程的threadLocalRandomProbe设置为probeGenerator 的值,如果probeGenerator 为0, 则取1;
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 判断是否为0
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
...
}
// ThreadLocalRandom类
private static final int PROBE_INCREMENT = 0x9e3779b9
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
//初始化
localInit();
return instance;
}
static final void localInit() {
// probeGenerator是个AtomicInteger
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
和probe相关的还有一个,如下是重新生成prod的方法:
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
longAccumulate方法
类似longAccumulate的方法还有doubleAccumulate,其内部实现差不多,这里仅针对longAccumulate进行分析
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// probe == 0 说明当前线程是第一次进入,则初始化probe作为hash值
int h;
if ((h = getProbe()) == 0) {
//强制初始化
ThreadLocalRandom.current(); // force initialization
h = getProbe();
// 为 true 表示没有竞争
wasUncontended = true;
}
// cas冲突标志,表示当前线程hash到的Cells数组的位置,做cas累加操作时与其它线程发生了冲突,cas失败;
// collide=true代表有冲突,collide=false代表无冲突
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
//这个主干if有三个分支 :
//1.主分支一:处理cells数组已经正常初始化了的情况(这个if分支处理add方法的四个条件中的3和4)
//2.主分支二:处理cells数组没有初始化或者长度为0的情况;(这个分支处理add方法的四个条件中的1和2)
//3.主分支三:处理cells数组没有初始化,并且其它线程正在执行对cells数组初始化的操作,及cellbusy=1, 则尝试将累加值通过cas累加到base上
//主分支一: 数组已初始化
if ((as = cells) != null && (n = as.length) > 0) {
// 内部小分支一:如果被hash到的位置为null,则用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁
if ((a = as[(n - 1) & h]) == null) {
//cellsBusy == 0 代表当前没有线程cells数组做修改
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
//如果cellsBusy=0无锁,则通过cas将cellsBusy设置为1加锁
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
//再次检查cells数组不为null,且长度不为空,且hash到的位置的Cell为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;
//如果created为false,说明上面指定的cells数组的位置cells[m%cells.length]已经有其它线程设置了cell了,继续执行循环。
continue; // Slot is now non-empty
}
}
//如果执行的当前行,代表cellsBusy=1,有线程正在更改cells数组,代表产生了冲突,将collide设置为false
collide = false;
}
// 内部小分支二:通过cas设置cells[m%cells.length]位置的Cell对象中的value值设置为v+x失败,说明已经发生竞争,将
// wasUncontended设置为true,跳出内部的if判断,最后重新计算一个新的probe,然后重新执行循环;
else if (!wasUncontended) // CAS already known to fail
//设置未竞争标志位true,继续执行,后面会算一个新的probe值,然后重新执行循环。
wasUncontended = true; // Continue after rehash
//内部小分支三:新的争用线程参与争用的情况:处理刚进入当前方法时threadLocalRandomProbe=0的情况,也就是当前线程第一次参与cell争用的cas失败,这
//里会尝试将x值加到cells[m%cells.length]的value ,如果成功直接退出
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//内部小分支四:分支3处理新的线程争用执行失败了,这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将
// collide设置为false,后面重新计算prob的值
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
//内部小分支五:如果发生了冲突collide=false,则设置其为true;会在最后重新计算hash值后,进入下一次for循环
else if (!collide)
//设置冲突标志,表示发生了冲突,需要再次生成hash,重试。 如果下次重试任然走到了改分支此时collide=true,!collide条件不成立,则走后一个分支
collide = true;
//内部小分支六:扩容cells数组,新参与cell争用的线程两次均失败,且符合扩容条件,会执行该分支
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
}
//为当前线程重新计算hash值
h = advanceProbe(h);
}
//分支二: 数组未初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
//初始化标志
boolean init = false;
try { // Initialize table
if (cells == as) {
//初始化cells数组,初始容量为2,并将x值通过hash&1,放到0个或第1个位置上
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
}
}
LongAdder
LongAdder有了Striped64之后,其代码就相对比较好理解了,例如我们使用LongAdder进行累加计算的时候:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/**
* 先判断cells数组有没有初始化,没有初始化则尝试cas对base进行更新
* 若有线程竞争,则进入下一个if,uncontended是表示无竞争的意思
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/**
* as == null || (m = as.length - 1) < 0 表示如下:
* 先判断as也就是cells数组有没有进行初始化,如果没有初始化就直接进入longAccumulate方法初始化
*/
if (as == null || (m = as.length - 1) < 0 ||
/**
* a = as[getProbe() & m]) == null表示如下:
* 1.先根据线程的hash值(getProbe()方法表示获取其线程hash值)& m得到index
* 2.然后判断此数组下标中是否有元素,如果没有元素就直接进入longAccumulate方法
*/
(a = as[getProbe() & m]) == null ||
/**
* !(uncontended = a.cas(v = a.value, v + x))表示如下:
* 1.若有元素,则通过CAS将需要加上的值x加到此数组下标上
* 2.若CAS返回失败则直接进入longAccumulate方法
*/
!(uncontended = a.cas(v = a.value, v + x)))
//处理涉及初始化、调整大小、创建新单元格和/或争用的更新情况。
//这种方法遇到了乐观重试代码常见的非模块化问题,它依赖于重新检查的读集。
longAccumulate(x, null, uncontended);
}
}
当我们需要计算总量的时候:
// 遍历cell增加到base上获得总量
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;
}
LongAccumulator
LongAccumulator的实现与LongAdder类似,不同点在于LongAccumulator可以传入LongBinaryOperator进行自定义计算:
public void accumulate(long x) {
Cell[] as; long b, v, r; int m; Cell a;
if ((as = cells) != null ||
(r = function.applyAsLong(b = base, x)) != b && !casBase(b, r)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended =
(r = function.applyAsLong(v = a.value, x)) == v ||
a.cas(v, r)))
longAccumulate(x, function, uncontended);
}
}
同理,在进行求和的时候,也需要传入的LongBinaryOperator进行协作计算:
public long get() {
Cell[] as = cells; Cell a;
long result = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
result = function.applyAsLong(result, a.value);
}
}
return result;
}
总结
AtomicInteger可以被用来当做并发场景下的累加器,存在大量并发的时候cas有可能在空转带来性能损耗。这个时候就可以使用Striped64的子类进行处理。其内部存在base与cell数组,并发存在的时候会自动挑选Cell单元进行存储,本质上相当于平摊了并发到各个Cell上。在统计的时候会将base与Cell中的数据进行加和。