首先介绍下 LongAddr
这个类的作用,这个类,是JDK8 以后提供的新的,用于并发计数的工具。以前我们并发的计数的话,使用的都是 AtomicLong
但是这个原子类,性能是有不足的,因为当存在大量并发的时候,可能很多的 CAS
操作都是失败的,所以线程会不断的重试CAS
,但是每一批中能成功的 CAS
操作只会有一个,当并发太高时候,会出现性能问题!
LongAddr
采用的方法是,共享热点数据分离的计数
,将一个数字的值拆分为一个数组!然后这个数组,后面修改这个值的时候,就修改这个数组的一部分!如果要得到这个数字的话,就要把这个值加起来!这样的技术,能让并发量大大提高!
热点数据分离
也是分段锁思想的一种延伸!将原本的对一个值的乐观锁,分离成多个值的乐观锁。
LongAddr 的优点:有很高性能的并发写的能力!
缺点:读取的性能不是很高效!而且可能不是很准确,因为是要取数组所有的值相加
---源码解析---
LongAddr
继承自 Striped64
这个类
Striped64
这个类,有一个 Cell
的内部类,这个类有个 Contended
注解,这个注解,用于将数据放置在不同的缓冲行中,这样可以避免伪共享问题,提供并发量(伪共享问题,后面单独说明)伪共享
Striped64
还有 cells
这个 Cell
数组,这个用于保存我们所说的,分离的数据
base
字段,在并发量不大,没有导致CAS失败的情况下,会直接修改这个值!
cellsBusy
,这个值为 1 代表我们正在修改cells的大小!
LongAddr 最核心的方法就是 add
方法了:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 如果cells数组不为空,或者,cas的修改base的值失败了
// cells数组不为空,那么代表曾经发生过并发修改失败了
// casBase修改失败,代表本次并发修改失败了
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
// 如果 cells数组是空的,直接进入longAccumulate
// 如果 cells数组小于1 直接进入
// 如果 cells 当前 slot的值为null 直接进入
// 如果 cas 当前 slot的值失败了直接进入
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
方法总结:
我们总是 如果
cells
不为NULL
,那么尝试修改cells
对应的slot
的值
如果cells
是空的,那么我们就尝试修改base
,base
修改失败了,代表当前有并发,也会进入longAccumulate
上面的add操作的核心方法是:longAccumulate
这个方法是Striped64提供的:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// getProbe 方法是当前线程的随机数!使用这个就能随机的修改cells的值了
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;
// 如果 cells 是空的,代表第一次发生并发!
if ((as = cells) != null && (n = as.length) > 0) {
// 进入此处的,都是cells不为空的
// 随机选择一个 slot ,如果是空的,则创建一个新的Cell添加进去!
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
// 设置 Busy
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;
}
// 这里是如果发生了锁竞争,并且当前的slot已经被占用了
// 进入这里 ,代表选择下一个 随机值,然后再来一次
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 如果两次随机值 都被占用了,那么就尝试设置下 新增 当前Slot的值
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 如果还是失败了,那么再判断下,cells 的个数能否添加了,
// cells 个数最大是 NCPU 的最近的2的倍数!
// 如果 cells 数目已经大于等于这个值了,那么只会 一直的改变slot
// 如果slot都被占用的话,就一直尝试去加在这个slot的值上
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 如果cells的值 不太够!小于 NCPU的话!会进入这里扩容cells
// 扩容的话,首先也是 设置 busy
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
}
// 上面的每次循环结束的话,都会改变下Probe
// 这样就实现了随机的修改cells的某个元素!
h = advanceProbe(h);
}
// 这里是初始化 Cells的逻辑,先检查 cellsBusy
// 如果有线程进入了Busy的话,就执行下一步,也就是casbase,再不行就轮训操作!
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
//默认的Cells大小为2
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
// 设置完毕,就可以退出了
if (init)
break;
}
// 如果 队列为空,并且有其他线程,正在实例化,cells的话
// 就去修改base的值!
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
总结下 add
方法:
假设我们现在对一个 LongAddr
对象,进行写操作
- 首先,是对
base
进行cas
的写,如果cas
的写失败了,那么代表有线程在进行竞争! - 此时
cells
是空的,方法执行进入longAccumulate
方法,这个方法,会实例化一个 新的cells
数组 - 之后如果有线程,执行
add
方法的时候,首先去修改cells
数组里面的对象的值。具体修改哪个值是按照线程的随机数,随机选出来的,但是cells
数组的大小是 CPU 数量的最近的2倍值!后面的并发修改,大部分都是 并行的修改cells
里面元素的Value
的值!这样相当于,将原本的一个锁,分化成了 多个锁,提高了并发度!
LongAddr
还有其他几个方法,基本上都是 间接的调用这个 add 方法:
increment
: add(1L)
decrement
:add(-1L)
为了获取,LongAddr 的结果,我们需要调用 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
的结果!
但是这个结果是不一定准确的!因为可能读取的时候,可能有的线程正在写数据!
reset
: 方法就是把 base 设置为 0 ,并且每个 cells 的元素的值都设置为0
除了LongAddr
之外,,Java还提供了 DoubleAdder
的实现,这个类的实现 调用的是 doubleAccumulate
方法,这个方法的实现和longAccumulate
的实现 非常类似!就是通过 doubleToRawLongBits
方法将都double
的值转换成Long
!