1.AtomicLong简单分析
AtomicLong是Java1.5时的一个基于CAS的原子类,通过CAS算法提供了非阻塞
的原子性操作,但是在超高并发下AtomicLong的性能就会非常低下。
我们来看一下AtmoicLong的 incrementAndGet() 方法的底层实现。(原子类的核心变量就是一个volatile修饰的变量,这里就不在看源码了)
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
底层调用的是UnSafe类的getAndAddLong()方法, (UnSafe底层核心都是native方法,即调用了C++的方法)可以看到,底层就是通过不断的自旋(循环),通过CAS一直尝试更新。
//UnSafe类中的方法
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2); //获取变量最新的值
//while循环 CAS失败一直不断尝试更新变量
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
compareAndSwapLong() 就是Unsafe类中的一个native方法,是一个CAS操作。
//UnSafe类中的方法
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
通过简单的源码分析,我们应该可以分析出来,在超高并发下为什么AtomicLong的性能并不高的原因,因为N多线程去操作一个变量会造成大量线程CAS失败,然后一直处于自旋状态(一直占用CPU),导致严重浪费CPU资源,降低了并发性
2.LongAdder与AtmoicLong介绍
先看一下两者的对比
我们知道,volatile是轻量级锁,可以解决多线程内存不可见的问题,对于一写多读,可以解决变量的同步问题,但是如果是多写,volatile无法解决线程安全问题。
例如,在多线程下的cnt++操作,就应该使用原子类或者加锁处理
// 基于CAS
AtmoicInteger cnt = new AtmoicInteger();
cnt.addAndAdd(n);
//加锁
synchronized(){
cnt ++;
}
而如果是JDK8,推荐使用LongAdder对象代替,因为他的性能比AtomicLong更好(减少乐观锁(CAS)重试次数)
LongAdder其他应用场景
对于Java项目中计数统计的一些需求,如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好。在大多数项目及开源组件中,计数统计最多的还是AtomicLong
,虽然是阿里这样说,但是我们仍然要根据使用场景来决定是否使用LongAdder。
LongAdder与AtmoicLong性能测试
我们可以看到,随着并发数量的增加,AtomicLong的性能是急剧下降的,LongAdder性能是AtomicLong的数倍。
3.LongAdder原理分析
先看一下LongAdder的原理图
设计思想
LongAdder使用分段
的方式降低CAS失败的频次.
我们知道,AtomicLong中有个内部变量value保存着实际的值,所有的操作都是针对该变量进行的。也就是说说,在高并发环境下,value变量其实是一个热点数据
,也就是多个线程竞争一个热点。
LongAdder的基本思路就是分散热点
,将value的值的新增操作分散到一个数组中,不同的线程会命中到数组的不同槽中,各个线程只会对自己槽中的那个value进行CAS
操作,这样热点就被分散了,冲突的概率就小很多。
LongAdder中有一个全局变量,volatile long base值,在并发不高的情况下都是通过CAS
来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。
例如当前类中base=10,有三个线程进行CAS原子性的 +1
操作,线程一执行成功,线程二、三执行失败后针对Cell[]数组中的Cell对象的value值进行CAS操作,此时两个线程对应的Cell对象的value都被设置为1,执行完毕后,统计累加数据: sum = 11 + 1 + 1 = 13
使用Contended注解消除伪共享
在Cell类上,标注了一个@Contended注解,此注解可以进行缓存行填充,从而解决伪共享问题,伪共享会导致缓存行失效,缓存一致性开销大。
什么是缓存伪共享?
@sun.misc.Contended static final class Cell{}
伪共享
是指多个线程同时读一个缓存行的不同变量时导致的CPU缓存失效,尽管这些变量之间没有关系,但由于在主存中临近,存在于同一个缓存行之中,它们的相互导致频繁的缓存未命中,引发性能下降。
解决伪共享的方法一般都是直接填充
,我们只需要保证不同线程的变量存在不同的缓存行即可,使用多余的字节来填充可以做到这一点,这样就不会出现伪共享的问题。例如在Disruptor队列
的设计中就有类似设计。
在Cell类上加的注释也有说明这一点
框中的翻译如下:
Cell
类是AtomicLong
添加了padded(via@sun.misc.compended)
来消除伪共享的变种版本。缓存行填充对于大多数原子来说是繁琐的,因为它们通常不规则地分散在内存中,因此彼此之间不会有太大的干扰。但是,驻留在数组中的原子对象往往彼此相邻,因此在没有这种预防措施的情况下,通常会共享缓存行数据(对性能有巨大的负面影响)。
3、惰性求值
LongAdder
只有在使用longValue()
获取当前累加值时才会真正的去结算计数的数据,longValue()
方法底层就是调用sum()
方法,对base
和Cell数组
的数据累加然后返回,做到数据写入和读取分离。
而AtomicLong
使用incrementAndGet()
每次都会返回long
类型的计数值,每次递增后还会伴随着数据返回,增加了额外的开销。
LongAdder求和原理
之前说了,AtomicLong
是多个线程针对单个热点值value进行原子操作。而LongAdder
是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
。
比如有三个线程同时对value增加1,那么value = 1 + 1 + 1 = 3
但是对于LongAdder
来说,内部有一个base
变量,一个Cell[]
数组。
base
变量:非竞争条件下,直接累加到该变量上
Cell[]
数组:竞争条件下,累加个各个线程自己的槽Cell[i]
中
最终结果的计算是下面这个形式:
4.LongAdder源码解析
4.1Striped64的内部结构
LongAdder继承了Striped64,一些核心的属性都在Striped64中,我们先来看一下Striped64类中的核心结构。
//内部类 cell内部类
@sun.misc.Contended static final class Cell {
//真正的value值
volatile long value;
Cell(long x) { value = x; }
//CAS操作对cell中的value进行赋值
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);
}
}
}
//当前计算机的CPU数量,用于控制cells数组长度的一个关键条件。
static final int NCPU = Runtime.getRuntime().availableProcessors();
//cells数组
transient volatile Cell[] cells;
//当没有发生过竞争时,数据会累加到base中 或者当cells扩容时,需要将数据写到base中
transient volatile long base;
/*
* 初始化cells数组或者对cells数组扩容时需要和获取一把锁(同步状态) CAS操作
* 0表示无锁状态,1表示其他线程已经持有锁了。
*/
transient volatile int cellsBusy;
/**
* Package-private default constructor
*/
Striped64() {
}
//通过CAS的方式更新cell中的数据
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
//通过CAS的方式获取锁(同步状态)
final boolean casCellsBusy() {
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
4.2add()方法
然后我们分析一下LongAdder类中的核心方法之一 add()方法的逻辑
add方法逻辑简单,主要是判断Cell数组是否被创建或者尝试写base,如果写Cell数组不为null或者CAS方式向base中写数据失败,那么接下来要执行核心的longAccumulate方法,否则就是Cell数组没有被初始化并且CAS方式写base成功,代表这次写数据成功。
// x表示要增加的值
public void add(long x) {
/*
* as 表示cells数组的引用
* b 表示获取的base值
* v 表示期望值
* m 表示cells数组的长度
* a 表示当前线程命中的cell
*/
Cell[] as; long b, v; int m; Cell a;
/*
* 进入if语句中的条件
* 条件1:
* > true当前cells数组已经创建 当前线程应该将数据写入到对应的cell中,进入if语句操作
* > false: 表示cells未初始化,当前线程应该将数据写到base中,执行条件二,写数据到base中
*
* 条件2:(取反)
* true 表示当前线程CAS写数据到base失败,需要进入if中操作
* false 表示当前线程CAS写数据成功,无需进入if中
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
/*
* 什么时候会进入if语句
* 1. 当前cells数组已经初始化,当前线程需要将数据写到对应的ecll中
* 2. CAS写base失败,可能需要重试或者初始化cells数组
*
* 即当前线程写数据失败时(向base中或者cell中写)会进入if语句中
*/
//true表示未竞争 false发生竞争
boolean uncontended = true;
/*
* 进入longAccumulate()中的条件
*
* 条件一:cells数组还没有初始化
* (as == null || (m = as.length - 1) < 0) 为true
*
*
* 条件二:当前线程对应的cells数组的位置为null
* getProobe()方法,底层调用的是UnSafe的本地方法getInt(),获取一个整型值,
* 和m(cells数组的长度-1)的结果就是[0, length - 1]刚好就是下标,注意cells的长度一定是2的次方数
* (跟HashMap的寻址方式类似)
* (a == as[getProobe() & m] == null)
*
* 条件三:当前线程对应的cells数组中的位置不为null,但是在对当前cell进行CAS设置值的时候失败,
* 表示对这一个cell写数据出现了竞争。
* !(uncontended = a.cas(v = a.value, v + x))
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
/*
* 那些情况会调用longAccumulate()
*
* 1、cells数组未初始化,也就是多线程写base发生了竞争。
* 2、cells数组已经初始化,但是当前线程对应的cell为空
* 3、cells数组已经初始化,并且当前线程对应的cell不为空,但是对这个cell进行CAS写时失败了
*/
longAccumulate(x, null, uncontended);
}
}
4.3longAccumulate()方法
通过上面的add()方法可以知道,最核心的方法就是longAccumulate()方法,它在LongAdder的父类Striped64中
总结
1.哪些情况会调用longAccumulate方法(前提条件都是当前线程写值(写到base或者cell中)失败)
- ①Cell数组未初始化。
- ②Cell数组初始化了,但是当前线程对应的Cell为NULL
- ③Cell数组初始化了,且当前线程对应的Cell不为NULL,但是发现有多个线程去写Cell,即发生了竞争。
进入longAccumulate方法后最终会执行怎样的逻辑?
- 如果Cell数组
未
初始化并且当前线程获取了锁,那么当前线程会初始化Cell数组,默认长度是2,并且为当前线程对应的Cell对象进行赋值
对应下面的CASE2 当前Cell数组正在初始化(即锁被其他线程获取)
,所以当前线程就去执行caseBase()方法通过CAS去更新base. 对应CASE3- 对于CASE1来说,又分为几种情况,由于整个过程比较复杂,这里找了一张流程图。
源码解析
/* 都有哪些情况会调用?
* 1、cells未初始化
* 2、cells初始化了,但是当前线程对应的cell为空,
* 3、cells初始化了,并且当前线程对应的cell不为空,但是当前的这个cell发生了写竞争
*/
//wasUncontended: 只有第三种情况即发生了竞争wasUncontended才会变为false。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// 表示当前线程的hash值
int h;
//为0,说明还没有分配哈希值,下面进行分配哈希值
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe(); //赋值给h
/*
* 这里为当前线程分配完哈希值后,为什么要修改 wasUncontended = true(表示未发生竞争)?
* 因为走到这里说明当前线程还没有分配哈希值,即默认是0,(0 & 任何数都是0),即当前线程
* 会被分配到索引为0的cell中,说明可能在索引为0的位置发生了写cell冲突,但是一旦分配
* 了线程哈希值后,可能下面线程就不会分配到索引为0的位置了,就将标志位修改为true了,表示没有发生竞争。
*/
wasUncontended = true; //标志位置为true
}
/*
* collide表示扩容意向,false一定不会扩容,true可能会扩容。
*/
boolean collide = false;
//死循环(自旋)
for (;;) {
/*
* as表示 cells数组的引用
* a 表示当前线程命中的cell
* n 表示cells数组长度
* v 表示期望值
*/
Cell[] as; Cell a; int n; long v;
//---------------------------CASE1 Start-------------------------------------
//进入条件: cells已经初始化了,当前线程应该将数据写入到对应的cell中
if ((as = cells) != null && (n = as.length) > 0) {
//CASE1.1 cells已经初始化,但是线程对应索引的cell未初始化
if ((a = as[(n - 1) & h]) == null) {
//true(cellsBusy = 0)表示当前锁未被占用, false表示被占用
if (cellsBusy == 0) {
//创建一个cell,初始值就是x。
Cell r = new Cell(x);
//只有锁标志位为0并且CAS获取锁成功才能进入下面的逻辑
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try {
/*
* rs cells数组的引用
* m cells数组的长度
* j 当前线程命中cell的下标
*/
Cell[] rs; int m, j;
/*
* 前两个条件恒成立
* 第三个条件判断是为了防止多线程下cell覆盖的问题
*/
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == nu
rs[j] = r; //将创建好的r赋值给当前位置
created = true; //修改标志位 表示已经创建
}
} finally {
//锁状态还原
cellsBusy = 0;
}
//已经创建了,直接退出。
if (created)
break;
//否则继续循环。
continue; // Slot is now non-empty
}
}
//扩容意向改为false。
collide = false;
}
//CASE1.2 只有一种情况wasUncontended为false,即当前cell不为null,
//并且发生了竞争。 后续重置hash值
else if (!wasUncontended)
wasUncontended = true;
/*
* CASE1.3 当前线程rehash过hash值,然后新命中的cell不为空
* 执行: CAS操作更新cell中的数据,如果更新成功,就退出
* 更新失败,继续循环。
*/
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
/*
* CASE1.4
* n(cells数组长度)大于NCPU,或者已经被其他线程扩容过了,
* 那么当先线程rehash后重试即可
*/
else if (n >= NCPU || cells != as)
collide = false; //扩容意向变为false
/*
* CASE 1.5
* 设置扩容意向为true。但是不一定发生扩容
*/
else if (!collide)
collide = true;
/*
* CASE1.6
* 真正扩容的逻辑
* 条件1、cellsBusy == 0表示无锁状态,当前线程可以去竞争锁
* 条件2、casCellsBusy()CAS获取锁的逻辑,获取锁成功可以执行扩容逻辑,
* false表示当前时刻有其他线程在做扩容相关的操作。
*/
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//防止多线程下重复扩容
if (cells == as) {
//扩容数组长度变为原来的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; // Retry with expanded table
}
//重置当前线程hash值
h = advanceProbe(h);
}
//----------------------------CASE1 END---------------------------------------
//----------------------------CASE2 Start-------------------------------------
/*
* 主要干的事:初始化cells数组
*
* 进入CASE2的前置条件 : cells数组还未初始化 as = cells = null
*
* 1、cellsBusy = 0 表示当前未加锁(初始化cells数组)
* 2、as == cells 如果单线程的情况下没有必要,但是多线程情况下,如果有线程
* 已经将cells初始化过了,此时as(null) != cells了,就进不来了。
* 3、 casCellsBusy() 即获取锁成功(去初始化cells数组)。
*
*/
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try {
// 还要判断一下 cells == as 也是防止其他线程已经初始化了cells,
// 导致当前线程又进行了初始化,造成了数据丢失。
if (cells == as) {
//初始化cell数组长度为2
Cell[] rs = new Cell[2];
//将x值赋给对应的Cell
rs[h & 1] = new Cell(x);
cells = rs; //赋值给cells
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//---------------------------CASE2 END----------------------------------------
//--------------------------CASE3 Start--------------------------------------
/*
* 1、当前cellsBusy加锁状态,表示其他线程正在初始化cells,所以当前线程将值累加到base中
* 2、cells被其他线程初始化后,当前线程需要将数据累加到base、
*/
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
}
}
4.4sum()方法
public long sum() {
Cell[] as = cells; Cell a;
//先将base中的值拿来
long sum = base;
//遍历Cells中每一个Cell,如果槽不为空,就累加Cell中的value值
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
//最终返回sum。
return sum;
}
5.总结
5.1AtomicLong可以弃用了吗
看上去LongAdder
的性能全面超越了AtomicLong
,而且阿里巴巴开发手册也提及到 推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数),但是我们真的就可以舍弃掉LongAdder
了吗?
当然不是,我们需要看场景来使用,如果是并发不太高的系统,使用AtomicLong
可能会更好一些,而且内存需求也会小一些。
我们看过sum()
方法后可以知道LongAdder
在统计的时候如果有并发更新,可能导致统计的数据有误差。
而在高并发统计计数的场景下,才更适合使用LongAdder
5.2收获
LongAdder
中最核心的思想就是利用空间来换时间,将热点value
分散成一个Cell列表来承接并发的CAS,以此来提升性能。
LongAdder
的原理及实现都很简单,但其设计的思想值得我们品味和学习。