CAS 底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。(不停的自旋,进入一个无限重复的循环中)

LongAdder
LongAdder,尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!(JDK1.8新特性)
- 在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的。
- 接着如果发现并发更新的线程数量过多,在发生竞争的情况下,会有一个Cell数组用于将不同线程的操作离散到不同的节点上去 ==(会根据需要扩容,最大为CPU核)==就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。
- 这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!
- 这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!
- 而且他内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。
- 这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。
- 最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。
AtomicLong VS LongAdder
public class LongAdderVSAtomicLongTest {
public static void main(String[] args) {
testAtomicLongVSLongAdder(1,10000000);
testAtomicLongVSLongAdder(10,10000000);
testAtomicLongVSLongAdder(20,10000000);
testAtomicLongVSLongAdder(30,10000000);
testAtomicLongVSLongAdder(40,10000000);
}
static void testAtomicLongVSLongAdder(final int threadCount,final int times){
try{
System.out.println("threadCount ="+threadCount+" , times ="+times);
long start = System.currentTimeMillis();
testLongAdder(threadCount,times);
System.out.println("LongAdder 耗时 "+(System.currentTimeMillis()-start)+"ms");
long start2=System.currentTimeMillis();
testAtomicLong(threadCount,times);
System.out.println("AtomicLong 耗时 "+(System.currentTimeMillis()-start2)+"ms");
}catch (InterruptedException e){
e.printStackTrace();
}
}
static void testAtomicLong(final int threadCount,final int times)throws InterruptedException{
AtomicLong atomicLong = new AtomicLong();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
list.add(new Thread(()->{
for (int j = 0; j < times; j++) {
atomicLong.incrementAndGet();
}
}));
}
for (Thread thread : list) {
thread.start();
}
for (Thread thread : list) {
thread.join();
}
}
static void testLongAdder(final int threadCount,final int times)throws InterruptedException{
LongAdder atomicLong = new LongAdder();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
list.add(new Thread(()->{
for (int j = 0; j < times; j++) {
atomicLong.increment();
}
}));
}
for (Thread thread : list) {
thread.start();
}
for (Thread thread : list) {
thread.join();
}
}
}
运行结果:
可以看出在多线程使用LongAdder之后程序运行速度比AtmoicLong快了很多,原因是:
LongAdder核心思想:将热点数据分离。
比如说它可以将AtomicLong内部的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,而最终的计数结果则为这个数组的求和累加,其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值。当前对象的实际值由所有的cell累计合成,这样热点就进行了有效地分离,并提高了并行度。这相当于将AtomicLong的单点的更新压力分担到各个节点上。在低并发的时候通过对base的直接更新,可以保障和AtomicLong的性能基本一致。而在高并发的时候通过分散提高了性能。
add方法
public void add(long x) {
//as 表示cells引用
//b 表示获取的base值
//v 表示期望值
//m 表示cells的数组长度
//a 表示当前线程命中的cells单元格
Cell[] as; long b, v; int m; Cell a;
/*
条件一:true->表示cells已经初始化过了,当前线程应该将数据写入到对应的cells中
false->表示cells未初始化,当前所有线程应该将数据写入到base中
条件二: false->表示cells已经初始化过了,当前线程应该将数据写入到对应的cells中
true->表示发生竞争了,可能需要重试或扩容
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
//true->表示为竞争,false->表示发生竞争
boolean uncontended = true;
/*
条件一二:true->说明cells未初始化,也就是多线程写base发生竞争
false->说明cells已经初始化,当前线程应该是找自己的cells写值
条件三:getProbe() ->获取当前线程的哈希值,m表示cells-1;
true->说明当前线程对应下标的cell为null;需要创建longAccumulate支持
false->说明当前线程对应的cell不为空,说明下一步想要将x值添加到cell中
条件四:true->表示cas失败,意味着当前线程对应的cell有竞争
false->表示case成功
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
/*
都有哪些情况会调用???
1,true->说明cells未初始化,也就是多线程写base发生竞争【重试或初始化cells数组】
2.说明当前线程对应下标的cell为null;需要创建longAccumulate支持
3,.true->表示cas失败,意味着当前线程对应的cell有竞争【重试或扩容】
*/
longAccumulate(x, null, uncontended);
}
}
add(long x):加上给定的x。
-
一开始只加给base,那么此时cells一定没有初始化,此时只会casBase,成功则返回。
-
casBase失败,意味着多线程写base发生竞争,进入longAccumulate(x, null, uncontended = true)重试或者初始化cells。
-
如果cells已经初始化过了,但是,当前线程对应下标的cell为空,需要创建。进入longAccumulate(x, null, uncontended = true)创建对应cell。
-
如果cells已经初始化过了,同时,当前线程对应的cell 不为空,cas给当前cell赋值,成功则返回。失败,意味着当前线程对应的cell 有竞争,进入longAccumulate(x, null, uncontended = false) 重试或者扩容cells。
进入longAccumulate方法的三种情况:
- 发生多线程竞争
- cells数组初始化后,当前线程对应的下标为null
- 初始化后且当前线程对应的cell不为空,当cas失败
longAccumulate方法
第一种情况:写base发生竞争,此时cells没有初始化,所以才会写到base,不走CASE1;
走Case2,判断有没有锁,没有锁的话,尝试加锁,成功加锁后执行初始化cells的逻辑。如果没有拿到锁,表示其它线程正在初始化cells,所以当前线程将值累加到base。
第二种情况:当前线程对应下标的cell为空,满足CASE1,到达CASE1.1中,创建一个Cell,加锁,如果成功,对应的位置其他线程没有设置过cell,将创建的cell插入相应位置。
第三种情况:当前线程对应下标的cell已经创建成功,但写入cell时发生竞争,到达CASE1.2,wasUncontended = true,把发生竞争线程的hash值rehash。
重置后走若CASE1.1,CASE1.2均不满足,到达CASE1.3【当前线程rehash过hash值,然后新命中的cell不为空】重试cas赋值+x一次,成功则退出。失败,扩容意向设置成true,rehash当前线程的hash值,再到1.3重试,还失败走CASE1.6扩容。
//wasUncontended ,只有cells初始化之后,并且当前线程竞争修改失败,才会是false
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h; //表示当前线程哈希值
if ((h = getProbe()) == 0) {//条件成立:说明当前线程还未分配哈希值
ThreadLocalRandom.current(); // force initialization
h = getProbe(); //取出当前线程哈希值赋值给h
//为什么???
//因为默认情况写入cells[0]位置,不把它当作一次真正的竞争,
wasUncontended = true;
}
boolean collide = false; //表示扩容意向,false->不一定扩容,true->一定扩容
for (;;) { //自旋
//as 表示cells引用
//a 表示当前线程命中的cell
//n 表示cells长度
//v 表示期望值
Cell[] as; Cell a; int n; long v;
/*
CASE1 表示cells已经初始化了,当前线程应该将数据写入对应的cell中
前 true->说明当前线程对应下标的cell为空,需要创建longAccmulate支持
后true->表示cas失败,意味着当前线程对应的cell有竞争【重试/扩容】
*/
if ((as = cells) != null && (n = as.length) > 0) {
//CASE1.1: 条件true表示说明当前线程对应下标的cell为null,需要创建new cell
if ((a = as[(n - 1) & h]) == null) {
//true->表示当前锁未被占用,false表示占用
if (cellsBusy == 0) { // Try to attach new Cell
//拿当前的x创建cell数组
Cell r = new Cell(x); // Optimistically create
//前:true->表示当前锁未被占用,false表示占用
//后: true->表示当前线程获取所成功,false->表示失败
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false; //是否获取成功
try {
//re 表示当前cells引用
//m 表示cells长度
//j 表示当前线程命中的下标
Cell[] rs; int m, j;
//前:恒成立
//rs[j = (m - 1) & h] == null) 为了防止其他线程初始化过该位置,当前线程在初始化导致数据丢失
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;
}
//CASE1.2
//只有一种情况 Uncontended=false:只有cells初始化之后,并且当前线储层竞争失败,才会是false,
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//CASE1.3
//当前线程rehash过哈希值,然后新命中的cell不为空
//true->写成功,退出自旋,
//false->表示rehash之后命中的新的cell也有竞争。重试一次
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//CASE1.4
//前:n 表示数组长度,n >= NCPU(内存)
true->扩容意向 改为false,表示不扩容,false->说明cells数组还可以扩容
//后:true->其他线程已经扩容过了,当前线程rehash之后重试即可,
else if (n >= NCPU || cells != as)
collide = false; //扩容意向改为不扩容即可
//CASE1.5
//!collide=true,设置扩容意向为true,但是不一定真的发生扩容
else if (!collide)
collide = true;
//CASE1.6 真正扩容的逻辑
//前: true->表示当前线程无锁状态,当前线程可以去竞争这把锁
//后: true->表示当前线程获取锁成功,可以去执行扩容操作,
//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
}
//重置当前线程哈希值
h = advanceProbe(h);
}
//CASE2 :前置条件cells还未初始化,as 未null
//条件一:true->表示未加锁
//条件二:cells == as ? 因为其他线程可能会在你给as复制之后修改了cells
//条件三:true表示获取锁成功,会把casCellsBusy=1,表示其他线程正在持有这把锁
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;
}
//CASE3:
//1.当前cellsBusy加锁状态,表示其他线程正在初始化cells,所以当前线程将数据累加到base
//2.cells被其他 线程初始化后,当前线程需要将数据累加到base
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
}
}
Striped64
Striped64是在java8中添加用来支持累加器的并发组件,它可以在并发环境下使用来做某种计数,Striped64的设计思路是在竞争激烈的时候尽量分散竞争,在实现上,Striped64维护了一个base Count和一个Cell数组,计数线程会首先试图更新base变量,如果成功则退出计数,否则会认为当前竞争是很激烈的,那么就会通过Cell数组来分散计数,Striped64根据线程来计算哈希,然后将不同的线程分散到不同的Cell数组的index上,然后这个线程的计数内容就会保存在该Cell的位置上面,基于这种设计,最后的总计数需要结合base以及散落在Cell数组中的计数内容。这种设计思路类似于java7的ConcurrentHashMap实现,也就是所谓的分段锁算法,ConcurrentHashMap会将记录根据key的hashCode来分散到不同的segment上,线程想要操作某个记录只需要锁住这个记录对应着的segment就可以了,而其他segment并不会被锁住,其他线程任然可以去操作其他的segment,这样就显著提高了并发度,虽然如此,java8中的ConcurrentHashMap实现已经抛弃了java7中分段锁的设计,而采用更为轻量级的CAS来协调并发,效率更佳