1 AtomicLong
AtomicLong是juc包下的原子类,在并发情况下计数操作使用AtomicLong可以保证数据的准确性。
下面是AtomicLong类的加1和减1操作的源码。
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
其核心就是调用unsafe类提供的getAndAddLong方法,追进去其源码如下:
//unsafe类的getAndAddLong方法源码
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;
}
这里var2是AtomicLong中的valueOffset,它代表的是AtomicLong中的value在内存的偏移量。这里通过var6获取其值,compareAndSwapLong是CAS操作,通过while操作来进行自旋来抢夺锁。但是在多线程竞争激烈的情况下,compareAndSwapLong可能会失败并且频繁的自旋操作会让cpu空转,从而会降低时间效率,这也是AtomicLong原子类的瓶颈所在。
2 AtomicLong和LongAdder的比较
通过上述的分析,我们知道AtomicLong之所以效率太低是因为竞争而导致的自旋使得cpu进行空转,为了解决这个问题,在Jdk1.8中中引入了一个LongAdder类(Doug Lea大神)来解决这个问题,在低更新争用下,这两个类具有相似的特征;而在高争用的情况下,这一类的预期吞吐量明显更高,而代价是空间消耗更高。
下面用一个简单的demo比较一下synchronized、AtomicLong、LongAdder和LongAccumulator的时间效率,后面会对LongAddr的源码进行分析。
public class Juc_LongAddrUp {
private final static int _1W = 10000;
private final static int THREAD_SIZE = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_SIZE);
CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_SIZE);
CountDownLatch countDownLatch3 = new CountDownLatch(THREAD_SIZE);
CountDownLatch countDownLatch4 = new CountDownLatch(THREAD_SIZE);
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByS();
}
} finally {
countDownLatch1.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println(clickNumber.number + "\t" + (endTime - startTime));
// =================================
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickLong();
}
} finally {
countDownLatch2.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println(clickNumber.atomicLong.get() + "\t" + (endTime - startTime));
// =================================
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByLongAddr();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println(clickNumber.longAdder.sum() + "\t" + (endTime - startTime));
// =================================
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByAccumulator();
}
} finally {
countDownLatch4.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println(clickNumber.longAccumulator.get() + "\t" + (endTime - startTime));
}
}
class ClickNumber {
int number = 0;
public synchronized void clickByS() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickLong() {
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void clickByLongAddr() {
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
public void clickByAccumulator() {
longAccumulator.accumulate(1);
}
}
在这个demo中,分别使用四种方式进行计数,每个测试都开了50个线程,并在每个线程中持续进行1000,000次加一操作,可以发现LongAdder方法的时间效率要远低于 synchronized和AtomicLong。
3 LongAdder源码导读
3.1 使用分段的方式降低并发的冲突
这里列出LongAdder的一张原理图
在设计思想上,LongAdder采用分段的方式来降低并发冲突的概率。
在AtomicLong中,实际存储数据的是一个value变量,所有的操作都会围绕该变量进行,也就是说高并发的情况下,多个线程会同时争抢该变量,获得成功的会进行操作,而失败的会不断进行自旋。
LongAdder的设计思路就是分散热点,即将value的操作分散到额外的数组,不同线程命中不同数组中的槽,各个线程只在自己命中的数据的槽中进行CAS操作,这样就会使得冲突大大减少。
LongAdder继承了Striped64这个类,这个类中有一个全局变量transient volatile long base,在并发力度不是特别高的情况下,CAS是直接通过操作该base的值进行的,如果CAS操作失败,则代表并发冲突,这时会额外使用Cell[]数组中的cell单元格来进行CAS操作, 从而减少冲突的概率。
而最后对LongAdder获取值的方式也是通过base+Cell数组所有单元格值的方式来统计LongAddr此时的值情况。
3.2 使用Contended注解来消除伪共享
在LongAdder的父类Striped64中存在一个transient volatile Cell[] cells数组,其长度始终是2的幂次方,其中在Striped64中定义的内部类Cell中,其用注解@sun.misc.Contended进行修饰,这个疏解可以进行缓填充,从而解决伪共享问题。伪共享会导致缓冲失效,缓存一致性开销变大。
@sun.misc.Contended static final class Cell
伪共享是指多个线程同时读写同一个缓存行的不同变量时导致cpu缓存失效,尽管这些变量之间没有任何关系,但是由于在主存中临近,存在于同一个缓存行中,所以相互覆盖会导致频繁的缓存未命中,导致性能低下。
解决伪共享的方法一般都是使用直接填充,我们只需要保证不同线程访存的变量存在于不同的CacheLine即可,使用多余的字节进行填充可以做到这一点。
缓存行填充对于大多数原子来说是繁琐的,因为它们大多不规则地分散在内存中,因此彼此之间不会有太大的干扰。但是,驻留在数组中的原子对象往往彼此相邻,因此在没有这种预防措施的情况下,通常会共享缓存行数据(对性能有巨大的负面影响)。
3.3 源码解读
下面将进行LongAdder的源码分析来观察下为什么LongAddr的时间效率会那么高。
首先解释下LongAdder的父类,Striped64的一些属性和方法的定义
- base:类似于AtomicLong中的全局value,没有竞争的情况下直接数据累加到base,或者在cells扩容时,也需要将数据写入到base
- collide:表示扩容意向,false表示一定不扩容,true表示可能扩容
- cellsBusy:初始化cells或者扩容cells时需要获取的锁,0表示无锁,1表示其他线程持有
- casCellsBusy():通过CAS操作,修改cellsBusy的值,CAS成功表示获取锁,返回true
- NCPU:表示当前计算机CPU的数量,Cell数组扩容时会用到
- getProbe(): 获取当前线程的hash值
- advanceProbe(): 重置当前线程的hash
LongAdder的加减操作如下:
public void increment() {
add(1L);
}
public void decrement() {
add(-1L);
}
可以观察到LongAddr的加减操作都是调用了add方法,追进去发现如下代码:
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(x, null, uncontended);
}
}
这里as是对cells数组的引用,b是base,m表示cells数组长度-1,a表示命中的cell单元格。
- (as = cells) != null:不为空表示直接对cells数组的某个槽位进行CAS操作;为空表示cells数组尚未初始化,直接在base上进行CAS操作
- 如果cells为空:先用casBase来判断是否可以进行与base的CAS操作,如果成功的话则直接跳出。如果casBase操作失败,则意味着同时有多个线程争抢、并发起冲突,需要对cells进行扩容
如果(as = cells) != null || !casBase(b = base, b + x)为true,表示要么已经开始使用cells数组或者并发争抢base起冲突需要进行cells的使用,进入下面这部分代码进行判断
(as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
- as == null:表示cells数组为空
- (m = as.length - 1) < 0: 表示数组长度为0
- (a = as[getProbe() & m]) == null:数组不为空则判断该线程命中的Cell是否为null
- !(uncontended = a.cas(v = a.value, v + x)):如果数组不为空并且命中的Cell不为null,就尝试对命中的Cell进行cas操作,成功则直接跳出,失败则将uncounted置为false
总结进入longAcccmulate的条件:cells为null或长度为0、以及命中的Cell为null进入longAccumulate(x, null, uncontended); uncontended为true,而只有命中Cell后再次进行CAS操作失败后才会将uncontended设置为false;
longAccumulate方法完整如下:
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;
}
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);
}
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;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
首先回顾下,什么时候会进入longAccumulate方法:
- cells数组为null,且当前线程竞争base失败
- cells数组不为null,但是当前线程命中的cells单元格为null
- cells数组不为null,且当前线程命中cell单元格也不为null,但是当前线程竞争cell单元格失败
方法的参数
- long x:要加的值
-
LongBinaryOperator fn : 默认为null
-
boolean wasUncontended: 表示当前线程竞争cell单元格是否成功,true为成功、false为失败(只有cells数组不为null,且命中不为null但是竞争失败才为false,其他所有情况均视为true)
下面来正式分析longAccumulate方法:
private static final long PROBE;
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
//设置为false,表示不会扩容(这是默认的)
boolean collide = false; // True if last slot nonempty
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
getProbe()方法是获取当前线程的哈希值,具体方法是通过UNSAFE.getInt()实现的
如果当前线程的hash值h == 0,0与任何数取模都是0,所以会固定在数组的第一个位置,这里做了优化,使用ThreadLocalRandom.current()重新计算了一个hash,最后设置为wasUncontended为true是因为想重新计算当前线程hash后的槽位是否是可以竞争成功的。
下面会进入一个for循环的自旋操作,这里简要展示该部分:
for (;;) {
Cell[] as; Cell a; int n; long v;
//CASE1:
if ((as = cells) != null && (n = as.length) > 0) {
}
//CASE2:
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
}
//CASE3:
else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
}
只有cells不为null,并且长度大于0才会进入CASE1,由于CASE1过于复杂,这里最后讲解
CASE2:
//CASE2
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
//CASE2.1
if (cells == as) {
//初始化
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
走到CASE2表示cells数组为null,这里通过cells == as判断当前的cells数组是否和以前一样(当前没有别的线程初始化cells数组,如果这个条件不成立表明cells数组已经被初始化),并且只有线程获取锁的时候才会执行CASE2(casCellsBusy())
进入后再通过cells == as判断有没有修改过cells,然后对其进行初始化,并将该线程命中的Cell单元格new一个Cell对象。
CASE3:
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
进入到这则表明cells数组正在进行初始化,即获取cells锁失败。再次尝试能不能获取base的锁,在base上进行CAS操作,如果获取到直接在base上操作,否则继续自旋转。
CASE1实现原理:
对其进行一点点的拆分:
1)首先就是判断,命中的Cell单元格为null,首先cellsBusy == 0会判断是否别的线程持有cells数组的锁。如果没有的话则会创建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;
}
获取锁后判断下该位置是否为null,如果为null则可以将new出来的Cell对象放在对应位置。created=true表示创建成功,就可以结束自旋了。
collide = false表示,只有当前线程命中的cell单元格为null但是当前线程没有获得锁时才会执行,将collide 设为false表明没有扩容想法,也正符合实际情况。
2)如果命中Cell不为空且存在竞争,将wasUncountended设置为true,同时在最下面重新hash获取新的命中单元
else if (!wasUncontended)
wasUncontended = true;
h = advanceProbe(h);
3)当cells数组的长度大于等于cpu的核数或者cells != as设置collide = false表明没有扩容意向。这里超过cpu的核数不再扩容是因为cpu的核数代表计算机的处理能力,当超过cpu的核数时,多出来的cell单元格没太大作用,反而占用空间。
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
4)如果扩容意向为false,就将collide设为true,之后执行advanceProbe方法重置哈希值。这里的操作是为了当collide为false时就不再执行后面的扩容操作了。
else if (!collide)
collide = true;
5)这里面其实是扩容逻辑,首先判断当前有无线程加锁,如果没有线程加锁那就通过casCellsBusy()方法尝试加锁,加锁成功之后将cellsBusy设为1,里面有一个if语句if (cells == as)是为了判断当前的cells数组和原来的数组是不是同一个(防止其他线程已经扩容过了),之后扩容为原来数组的两倍,之后将旧数组中的值拷贝到新数组中去,设置cellsBusy为0释放锁,设置collide为false表明已经没有扩容意向了,之后自旋。
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
}
LongAdder的sum:当我们使用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;
}
内部实现就是将base和所有不为null的cell单元格内的数值求和。
LongAdder的核心思想就是空间换时间,将一个热点数据分散成cells数组从而减小冲突,以此来提升性能。
如果对于并发要求不高但是对精确要求很高的情况下还是建议使用AtomicLong。
如果对于并发要求很高但是对精确要求不高的情况下建议使用LongAdder,比如点赞等(因为在高并发下如果有并发更新,则利用sum汇总时数据可能不准确)