AtomicLong性能瓶颈分析
AtomicLong关键代码他用得是Unsafe的代码:
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
思考这段代码或者说cas存在什么问题,cas的愿意就是底层lock前置使得cpu的指令具有原子性,在多个线程进行同时修改只能一个线程能够成功, 那么我们就假设有16个线程(为啥16个呃,一般16核所以同时能够运行16个线程),那么15个线程就需要失败 ,所以我们可以粗略的估计一下 16个线程 要循环几次才能累加16次, 161514.。。。。, 这个叫啥数学上的排列组合吗? 那么我们的cpu是不是白白的浪费了,多个线程一直在空转。
造成整个问题的原因在于?
呃,因为累加的地址空间值只有一位,那么我们想个方法把累加计数的地址空间变成多份,最后在统计结果时候把多个地址空间累加起来,就可以得到总的计数。
好的,那么问题来了,是不是这个地址空间越多越好呢?
依据就是根据cpu的核数, 因为cas最好的情况的就是不要发生空转,也就是cas竞争失败,所以一个地址变量最好一个线程来运行,那就说明一个地址变量代表一个线程, 我们知道同时指向的线程数是和cpu的核数对应的,所以最好的情况就是地址空间开辟为核数的个数,但是如果线程并发个数很少,就只有一个?需要一开始就开辟16个地址空间吗,所以。。。,得有个升级过程,比如cas失败那就去开辟空间。。。
所以AtomicLong的缺点就是 不能有效利用cpu核数, 线程多了之后会导致cpu空转,耗费cpu资源。
案例证明
public class TestLongAddr {
public static void main(String[] args) throws InterruptedException {
/*线程个数*/
int threadCount = 100;
/*每个线程的累加个数*/
int count = 1000000;
AtomicLong atomicLong = new AtomicLong();
LongAdder longAdder = new LongAdder();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
new Thread(()->{
for (int i1 = 0; i1 < count; i1++) {
atomicLong.addAndGet(1);
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(atomicLong.get() + " atomiclong 耗时 " + (System.currentTimeMillis() - startTime));
CountDownLatch countDownLatchLongAddr = new CountDownLatch(threadCount);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
new Thread(()->{
for (int i1 = 0; i1 < count; i1++) {
longAdder.add(1);
}
countDownLatchLongAddr.countDown();
}).start();
}
countDownLatchLongAddr.await();
System.out.println(longAdder.longValue() + " longaddr 耗时 " + (System.currentTimeMillis() - startTime));
}
}
100000000 atomiclong 耗时 2292
100000000 longaddr 耗时 421
LongAdder源码分析
上图吧先
大概就是如果基本技术base cas失败就会开辟数组空间,然后这个线程的id值取模得到cell数组下标去进行累加。
基本数据结构
static final int NCPU = Runtime.getRuntime().availableProcessors(); //可用的处理器个数
/**
* Table of cells. When non-null, size is a power of 2.
*/
transient volatile Cell[] cells; //扩展计数
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base; //基本计数
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
transient volatile int cellsBusy; //调整大小或者创建cells数组的时候的cas锁变量
这个有个地方值得只注意 cell数组是volatile的, 这样cell数组的扩容赋值之后别的线程就可以立马感知到,也就是可见性, 那问题来了,我对cell数组里面的元素累加的时候呢? 所以依然需要可见性, Cell的类结构如下
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);
}
}
}
public void add()
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;//这里cells是volatile的所以重新赋值保证了一定读到最新的
if ((as = cells) != null || !casBase(b = base, b + x)) { //如果cell数组已经扩展了 或者 cas成功,说明没有竞争就不会继续往下面走
boolean uncontended = true; //true表示没有竞争
if (as == null || (m = as.length - 1) < 0 || //as数组没有初始化, 或者初始化了但是长度为0
(a = as[getProbe() & m]) == null || //进行下标的定位 采用按位于的方式,比%的速度更快, 当前线程对应的槽位cell为null
!(uncontended = a.cas(v = a.value, v + x))) //当前下标cell不为null,尝试进行cas操作
longAccumulate(x, null, uncontended); //uncontended=false 说明当前有竞争,因为cas失败了, 所以总结有几种情况会进入到longAccumulate
}
}
预期总结如何才会进入longAccumulate 不如总结如何才不进入。。。
1、base 基本计数cas成功
2、cell槽位cas成功
final void longAccumulate()
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) { //数组第0个元素, 0和0与才会等于0,所以这里就没有进行与了,直接判断, 不进行初始化默认hash为0
ThreadLocalRandom.current(); // force initialization 改变线程hash
h = getProbe(); //这里做了优化,使用ThreadLocalRandom为当前线程重新赋予一个hash值,为啥这样呢。。因为进入这个方法是有可能有竞争的,所以重新hash一下到别的槽位,应该是没有竞争
wasUncontended = true; //重新hash到别的cell, 默认是没有竞争的
}
boolean collide = false; // True if last slot nonempty 扩容意向。false表示不会扩容,true表示可能会扩容
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) { //cells数组不为null, 且长度大于0, 说明已经进行了初始化
if ((a = as[(n - 1) & h]) == null) { //如果计算出来的槽位没有创建
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create 乐观的创建,待会需要进行cas
if (cellsBusy == 0 && casCellsBusy()) { //如果现在还是没有别人获取到锁,获取操作cells数组的执行权
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; //这里说明创建的cell本身就已经默认是x值了,所以直接break
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash 这个cell位置竞争激烈,需要进行重新hash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x)))) //cell位置已经有值了,直接进行cas操作
break;
else if (n >= NCPU || cells != as) //是否已经到达CPU核数上限
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {//获取cell的锁权限
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);// 重新获取hash值
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) { //cells == as 就是代表没有别的线程进行初始化,都是null, casCellsBusy() cas争取获取初始化数组的资格
boolean init = false;
try { // Initialize table
if (cells == as) { //确保没有被初始化?这里这个判断需要好好思考,因为casCellsBusy() 与 cellsBusy = 0 所以这个casCellsBusy()可以多次cas成功,但是我们只需要第一个线程初始化就行
Cell[] rs = new Cell[2]; //第一次初始化默认为2个长度数组
rs[h & 1] = new Cell(x);//这里的x就是add(x)传入的单位,呃,因为当前线程的就是累加,所以当当前线程拥有了锁去初始化数组权限的时候直接赋值x就行
cells = rs; //volatile赋值
init = true; //初始化完成
}
} finally {
cellsBusy = 0; //初始化已经完成了,把初始化权限释放掉
}
if (init)
break; //初始化完成跳出循环
}
else if (casBase(v = base, ((fn == null) ? v + x : //进入这里的逻辑说明别人已经在对cell进行初始化了,所以当前线程可以进行一下base的cas累加,说不定base已经不激烈了。
fn.applyAsLong(v, x))))
break; // Fall back on using base cas 累加到base上成功,直接跳出循环
}
}
上图
总结
分段思想,充分利用cpu。