简述
LongAdder是JDK8新增的一个用于高并发场景下进行统计的类。以前AtomicInteger、AtomicLong等通过使用CAS的方式来更新变量,比synchronized这些阻塞算法拥有更好的性能。但是在高并发情况下,大量线程同时去更新一个变量,任意一个时间点只有一个线程能够成功,绝大部分的线程在尝试更新失败后,会通过自旋的方式再次进行尝试,严重占用了CPU的时间片。导致AtomicInteger、AtomicLong在高并发场景下的性能严重降低,所以产生了LongAdder来满足高并发场景下的统计。
LongAdder原理
没有竞争的时候,线程会对base里面的value进行修改
一旦出现高并发场景下的多线程竞争,那么LongAdder会初始化一个cell数组,然后对每个线程获取对应的hash值,之后通过hash & (size -1)[size为cell数组的长度]将每个线程定位到对应的cell单元格,之后这个线程将值写入对应的cell单元格中的value,之后将所有cell单元格的value和base中的value进行累加求和得到最终的值。
在整个过程中,涉及到cell的初始化,线程定位到单元格,以及cell数组的扩容等一系列过程。接下来,我们会对LongAdder的add方法源码进行分析,来学习LongAdder在处理高并发场景下的思想
源码分析
首先,LongAdder继承Striped64这个类。在这个类里面定义了如下关键变量
// 获取当前机器的CPU线程数量,如果是8核16线程,那么NCPU就是16
static final int NCPU = Runtime.getRuntime().availableProcessors();
// cell数组
transient volatile Cell[] cells;
// 没有竞争时写入的base值
transient volatile long base;
// cell数组对应的锁 0表示当前cell数组没有线程使用,1表示当前数组已经有线程占用
transient volatile int cellsBusy;
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
// 通过CAS来获取当前cell数组的锁
final boolean casCellsBusy() {
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
//获取当前线程的hash值
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
下面是LongAdder的add方法
/*
as是当前cell数组的引用
b代表base的值
m代表cell数组的长度
a代表当前线程hash之后定位到的cell单元格
v代表期望值
uncontended=true 代表当前线程对应的cell单元格CAS成功,uncontended=false表示当前线程对应的cell单元格CAS写入失败,出现竞争。
*/
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方法的三种状态
1,as == null || (m = as.length - 1) 当前cell数组为空,没有初始化
2,a = as[getProbe() & m]) == null 获取当前线程的hash值然后和数组长度进行&运算得到对应的cell单元格为空
3,uncontended = a.cas(v = a.value, v + x) 当前线程对应的cell单元格CAS失败,出现竞争
*/
longAccumulate(x, null, uncontended);
}
}
下面是longAccumulate方法
*/
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
//是否扩容标志位,false表示一定不扩容,true表示可能会扩容
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// CASE-1:当前cell数组已经初始化
if ((as = cells) != null && (n = as.length) > 0) {
// CASE-1.1 当前线程对应的cell单元格为空
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;
}
// CASE-1.2 当前线程对应的cell单元格CAS写入数据失败出现竞争。
else if (!wasUncontended)
wasUncontended = true; // Continue after rehash
// CASE-1.3 当前线程对应的cell单元格CAS写入失败之后重试,如果成功则跳出当前循环
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// CASE-1.4 如果当前cell长度已经超过当前机器的CPU数量,拒绝扩容
else if (n >= NCPU || cells != as)
collide = false;
else if (!collide)
collide = true;
// 对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
}
h = advanceProbe(h);
}
// CASE-2 cell数组进行初始化
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;
}
// CASE-3 但cell数组为空,并且获取cellBusy失败再次进行重试CAS写入base
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
场景分析
1,出现多线程竞争,就是多个线程写入base
就是add方法的判断条件!casBase(b = base, b + x)
为false,取反之后为true,进入下一个if
1.1 此时cell数组没有进行初始化
就是as == null || (m = as.length - 1) < 0
为true,进入longAccumulate方法的
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
/*cellBusy为0表示没有线程获取到当前cell的锁,
通过CAS将cellBusy更新为1获取到锁开始初始化cell数组
init cell数组是否初始化完成的标志位
*/
boolean init = false;
try {
/*
再次判断当前cell数组是否为前面的as引用。这里会出现一种情况,就是线程1执行到cellBusy=0之后因为系统调度的原因让出CPU时间,
这时线程2更新cellBusy成功获取到锁并初始化cell数组成功并释放锁,将cellBusy更新为0。
然后线程1得到CPU时间开始更新cellBusy值获取锁成功,但此时cell数组已经初始化完成,此时线程1再次进行初始化会覆盖掉线程2已经初始化的cell数组。
其他地方的判断都是防止系统调度原因防止线程再次操作而覆盖。
*/
if (cells == as) {
// 初始化一个长度为2的数组
Cell[] rs = new Cell[2];
//当前线程的hash值和1进行&运算,结果只会是0和1所以这个线程只会定位到cell[0]或者cell[1]的单元格
rs[h & 1] = new Cell(x);
cells = rs;
// init为true表示cell数组初始化完成
init = true;
}
} finally {
// 更新cellBusy为0,释放锁
cellsBusy = 0;
}
// cell数组初始化完成,跳出当前循环,开始下一轮循环
if (init)
break;
}
如果此时cellBusy为0但是通过CAS将cellBusy为1失败,说明已经有线程在初始化cell数组,那么就进入下一个判断
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
// 再次尝试写入base,如果此时写入成功,就跳出,否则进入下一个循环。
1.2 当前线程定位的cell单元格不存在为空(a = as[getProbe() & m]) == null
getProbe()方法获取的就是当前线程的hash值。这个判断条件满足一个前提就是add方法的(as = cells) != null
为true。此时进入longAccumulate的
/*
老规矩 二次判断
*/
if ((as = cells) != null && (n = as.length) > 0) {
// 二次判断
if ((a = as[(n - 1) & h]) == null) {
// 当前cellBusy为0 没有线程修改cell数组
if (cellsBusy == 0) {
// 创建一个cell对象
Cell r = new Cell(x);
// 获取当前cell数组的锁
if (cellsBusy == 0 && casCellsBusy()) {
// cell对象是否创建完成的标志位
boolean created = false;
try {
/*
再次判断,老样子,防止系统调度原因出现线程的二次修改
*/
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
// cell单元格创建完成,更新created为true
created = true;
}
} finally {
// 释放锁
cellsBusy = 0;
}
// 跳出循环,走下面的else if
if (created)
break;
continue;
}
}
collide = false;
}
h = advanceProbe(h);
}
1.3 当前线程对应的cell单元格出现了竞争,多个线程通过hash之后都定位到同一个cell单元格,对应add方法的
!(uncontended = a.cas(v = a.value, v + x))
此时进入longAccumulate方法的
else if (!wasUncontended)
wasUncontended = true;
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 如果cell数组长度大于CPU线程数量,停止扩容
else if (n >= NCPU || cells != as)
collide = false;
else if (!collide)
collide = true;
// 获取到cell数组的锁,开始执行数组扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 老样子 二次判断
if (cells == as) {
// cell数组扩容为原来2倍
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;
}
1.4 LongAdder的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;
}
在这个方法,遍历cell数组的value并和base进行求和,最终得到sum值。
总结
通过上面的源码分析我们可以看到,LongAdder通过每个线程对应自己的cell单元格来降低高并发下的竞争写问题。同时cell数组长度一旦大于CPU数量就停止扩容来最大程度使用并发数,因为一台机器真正并发的线程就等于CPU的数量,进而提高性能。
在里面处理方法有读锁和写锁分离,将锁粒度降低,最大程度利用CPU的多线程并发处理能力,
将对单个base的并发写散列开,每个线程对应一个cell单元格来降低并发。这个处理方法我们在以后的高并发场景下可以学习借鉴。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。