本文只针对原子累加器的加法API进行分析,正所谓万变不离其宗,笔者昨日在观看教学视频后,忽有所悟,因而在此刻撰下一篇
LongAddr的源码解析文章,供码界前辈后进一同阅览。
为什么要引入LongAddr
在JUC包下,大师Doug Lea已经发明了原子类,AtomicLong自然也可以用来做累加,那么为什么还要引入LongAddr呢?让我们来简单看一看AtomicLong的具体实现。
很显然,
AtomicLong使用了Unsafe包下的CAS操作来实现替换的,然而,CAS在多线程竞争的情况下效率并不高,大量的线程占据CPU资源后就会发生空转。
因此,为了提高效率就引入LongAddr
LongAddr的核心
LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。由于Cells占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。
Cell
想要了解LongAddr首先就要了解它的field,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;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
@Contended注解是用来防止伪缓存行共享的,一个缓存行的长度是64byte,而一个Cell的大小是24byte(对象头16byte+属性-value-8byte)。Cell本质上也是使用Unsafe包下的CAS操作来完成线程安全的操作(比如++操作)
NCPU
CAS操作本质上是一种自旋盲循环,自旋盲循环是要消耗CPU资源的,而availableProcessors()这个方法就可以获取JVM的可用核心数。
Cells
Cell数组,这是LongAddr提升性能的关键
base
在没有竞争的时候,就更新这个值
cellsBusy
初始化或者扩容时的CAS标志位
LongAddr的increment
add
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
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);
}
}
要搞清楚一个方法做什么,我们应该用解构的思想来看待它,那么在这个add方法中,它一共有5个临时变量,两个成员变量(LongAddr的父类)
第一个if
不得不说,第一个if中的逻辑极其巧妙,短短一行代码中就蕴含了极其丰富的逻辑,值得我们细细研究。
- (as = cells) != null || !casBase(b = base,b+x)
由于
cells需要的内存比较大,所以是惰性加载的,那么在第一次累加的时候,as!=null一定是false,但这不意味着后续的cas操作一定能成功,假设有线程A、B,同时累加,而由于cells是惰性加载的,那cells此刻一定是null,就执行到if语句的后半段,那么很显然A、B线程中只有一个线程会执行cas操作成功。那么那个不成功的就会进入下个阶段的判断 - as == null || (m = as.length - 1)
- a = as[getProbe() & m]) == null
这两行判断决定了
当前线程应该访问cells数组里面的哪一个Cell元素,如果当前线程映射的的Cell元素存在,就执行代码 - !(uncontended = a.cas(v = a.value, v + x))
这行代码显然是使用
cas操作去更新当前线程对应的cells数组中Cell元素的value。 如果当前线程映射的Cell元素不存在,或者cas操作失败,那么就执行这行代码 - longAccumulate(x, null, uncontended);
在分析
longAccumulate方法之前,我们可以归纳一下在add方法中这些临时变量有什么用,as代表了cells数组,m->cells数组的长度-1,b->base,a->Cell(cells数组中当前线程对应的Cell元素) v->cells数组中当前线程对应的Cell元素的值。此外,当前线程访问cells中的哪一个Cell元素由getProbe() & m来决定,我们已经知道m代表数组的长度-1,那么getProbe方法是获取当前线程中threadLocalRandomProbe的值,这个值一开始为0,在longAccumuulate方法中会进行初始化,并且当前线程通过分配到Cell元素的cas操作来保证value更新的原子性。
longAccumulate
这个方法是LongAddr的核心,逻辑极其复杂
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//这里是在对当前线程的Probe的值进行初始化
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;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
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 = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
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);
}
// cells 数组的初始化
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;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
由于此处代码极其复杂,笔者只能尽自己所能将代码逻辑说得通顺一些,但是决计做不到尽善尽美,所以看待笔者对代码的分析的时候,一定要本着
怀疑与批判的逻辑来阅读,否则因为笔者的一些疏忽而造成误人子弟的后果,反而就不美了。话说回来,前辈后进们若是发现有不美之处,欢迎斧正指出。
cells 数组的初始化
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;
}
casCellsBusy方法,通过cas操作将cellBusy设置为1代表cells正在经历初始化或者扩容,当然在上面这块代码中,cells正在初始化
init标志位,一开始设置为false,代表cells还没有初始化,初始化完成后,将cellsBusy设置为0,可以看做一种解锁操作,最后根据init代表初始化完成,从而退出循环。
增加一个cell元素
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
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 = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
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);
}
这块代码的语境应该是,某一个线程计算出的probe在cells数组中为null或者说probe大于当前cells的数组长度,那么就要添加往cells数组中添加Cell元素,这个操作是需要上锁的,cas乐观锁。
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
这段代码在使用cas操作对当前线程对应的Cell元素进行value的替换,如果成功就退出循环。
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
前文已经提过,cas操作的最佳性能只有在执行cas操作的线程数和CPU数相等的时候才能达到,所以才会有这个判断,因此cells数组的长度取决于CPU的数量,(准确地来说,是JVM可用核心数)
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
}
这一块代码是在执行扩容操作,和初始化、以及添加Cell元素一样,扩容操作同样需要加上CAS锁。
h = advanceProbe(h);
这是重新计算hash值,该计算发生在当前线程计算出的偏移量在cells数组中对应的Cell在有竞争的情况下就要的重新计算hash值,寻找一个空闲的Cell