前面介绍了 AtomicInteger 后,就没有继续介绍 AtomicLong,因为这两个类基本是类似的,懂了第一个那第二个就不在话下了。
根据前面学习过 AtomicInteger 代码,我们可以知道类内部有一个变量 value 专门保存当前的值。然后在增加或减少指定数值时,会不断 CAS 直到成功。
那这样的话,value 作为一个热点值,会被频繁的修改。如果并发量低的情况下,CAS 失败的次数较少,性能可以杠杠的。但是在高并发环境下,N 条线程一起来执行,这时,线程不断的自旋尝试修改,造成大量的 CAS 失败,在这种不断失败的情况下,就造成了性能瓶颈。
那有没有减少线程冲突,提升性能的方案呢?别说,还真有这种方案。LongAdder 类就是为减少线程间冲突,提升性能而出现的方案。具体是使用分段的方案,根据 CPU 核数进行确定具体分为几个槽,每条线程会命中一个的槽,然后在自己的槽中 CAS 修改变量 value。
这样做,可以有效的将 AtomicInteger 中热点变量 value 从一个分为多个,有效的减少了线程的冲突,性能也相对应得到提升。
举个例子:假设我们开了一个快餐店(AtomicInteger),生意火爆,营业额(value)在不断的变化。生意火爆,很多人都买不到快餐。这时想到一个开分店的方式,就开了 3 家分店来分流(LongAdder),每家分店都有自己的营业额(value),每天晚上进行一天的营业额汇总,即可知道今天赚了多少。
介绍基本原理,我们来学习一下 LongAdder 是如何实现的。
相关类图:
从上图,我们可以看出 LongAdder 继承自 Striped64 这个抽象类,所有也有会 Number 中数据转型的方法。
我们先看看父类 Striped64 :
内部类 Cell 内部有字段 value,该值和 AtomicInteger 的 value 一样会被多条线程竞争修改。
@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);
}
//以下省略
}
重要字段
/** CPU 核数 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* 这就是槽,一般初始化大小是 2 的幂次方
*/
transient volatile Cell[] cells;
/**
* 这个很重要,如果没有发生线程冲突的情况下,直接在该值
* 原基础上加,发生竞争则在槽中 value 修改
*/
transient volatile long base;
/**
* 这时一个锁,当槽初始化或扩容时,会锁住当前容器
*/
transient volatile int cellsBusy;
因为子类又调用了父类的方法,所有我们先看看子类的核心方法
LongAdder 类的核心方法:
/**
* 增加多少
*/
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/**
* cells 为空时,说明数组还未初始化
* casBase(b = base, b + x) 是指存在线程冲突
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/**判断数组有没被初始化过
* 初始化过之后直接针对 Cell 进行 CAS
*/
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);
}
}
父类 Striped64 的核心方法
方法被定义成了 final 。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//这一段都是在生成一个 哈希值
//用于找该线程对应的槽
int h;
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;
}
//created 为true 则为成功,跳出循环
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);
}
//槽的初始化,cellsBusy == 0 是将槽加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
//构建的数组必须是 2 的幂次方
Cell[] rs = new Cell[2];
//将当前值 X 放入槽中替换某个 value
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 是考虑了各种情况,比如:槽正在初始化过程,槽的扩容等。
注意:
base 变量在没有线程冲突的情况下是在他基础上累加的,还有一种情况就是线程冲突厉害,槽还在初始化过程中,也会尝试对 base 变量进行修改。
最后,需要将各个槽中的数值和 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;
}
求和方法很简单,就是一个循环,long sum = base; 在线程没有冲突时,是直接在该变量值基础上进行增加或减少操作的,所以将各个槽的值累加的情况下,也需要将 base 的数值加入才可以获得准确的返回。
最后:
DoubleAdder 和 LongAdder 的方法基本类似,只是多了一个转型的方法,其他都一样,就不在介绍那个类了。LongAdder 是在 AtomicLong 的优化版本吧,但是也无法完全替代 AtomicLong 的用途。不过从 LongAdder 上我们可以学习到以空间换时间的思路,也算是收获满满啦。
有兴趣的同学欢迎关注公众号