目录
分组来给大家讲解相关原子类的常用api使用,不会全部都讲完,只是抽取几个比较经典的讲一下案例使用
基本类型原子类
- AtomicInteger
- AtomicBoolean
- AtomicLong
问题案例:为什么结果是不准呢,因为里面的50个线程还没有跑完,main线程去拿结果,就会导致没有计算完拿到了错误结果
要等前面50个线程跑完,让main线程sleep一段时间也可以,但是不太好,怎么解决呢?
countDownLatch,50个线程,等到50个线程都跑完,后面的线程拿到最终值
数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
案例演示:数组根据下标获取数值
引用类型原子类
- AtomicReference
- AtomicStampedReference(利用版本号,解决ABA问题,能够知道修改了几次)
- AtomicMarkableReference(将状态戳简化为true/false,解决是否修改过)
案例演示:同时间不同线程拿到的都是false初始化值,但是a线程执行完之后,发生了改变,b线程再去修改时,发现值被使用过,再次修改失败
对象的属性修改原子类
- AtomicLongFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicReferenceFieldUpdater
作用:以一种线程安全的方式操作非线程安全对象内的某些字段
不用原子操作类以前:效果虽然能实现,但是加了synchronized
现在不加锁,针对引用类的字段进行原子性操作:效果也一致
上面是针对integer,那么引用类中的字段呢?来,让我们接着奏乐接着舞
AtomicReferenceFieldUpdater:基于反射的实用程序,可以对指定类的指定volatile引用字段进行原子更新。
需求:多线程并发调用一个类的初始化方法,如果未被初始化,将执行初始化工作,要求只能被初始化一次,只有一个线程操作成功
案例实现如下:
原子操作增强类
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
这几个是java8出来的增强类,前面十二个是java5的原子操作类
按照以上图片红框内容,我们来实现一个热点商品点赞数,点赞累加统计,要求吞吐量极高,不要求实时精准的(98%)需求(因为正在统计时,也在实时增加点赞数)
单线程简单使用:
以上案例可以总结出:LongAdder支持简单的加法计算,计算初始量从0开始,而LongAccumulator支持自定义计算公式,且初始值也可以自定义
多线程案例:50个线程,每个线程100w次,统计总点赞数
内部类:
class ClickNumber{
int number = 0;
public synchronized void clickSynchronized(){
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickAtomicLong(){
atomicLong.incrementAndGet();
}
LongAdder longAdder = new LongAdder();
public void clickLongAdder(){
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
public void clickLongAccumulator(){
longAccumulator.accumulate(1);
}
}
实现类:
public static final int _1w = 10000;
public static final int threadNumber = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
final CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
final CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
final CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
final CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(()->{
try {
for (int j = 1; j <= 100 * _1w; j++) {
clickNumber.clickSynchronized();
}
} finally {
countDownLatch1.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime-startTime)+"毫秒 \t clickSynchronized:"+clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(()->{
try {
for (int j = 1; j <= 100 * _1w; j++) {
clickNumber.clickAtomicLong();
}
} finally {
countDownLatch2.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime-startTime)+"毫秒 \t clickAtomicLong:"+clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(()->{
try {
for (int j = 1; j <= 100 * _1w; j++) {
clickNumber.clickLongAdder();
}
} finally {
countDownLatch3.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime-startTime)+"毫秒 \t clickLongAdder:"+clickNumber.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(()->{
try {
for (int j = 1; j <= 100 * _1w; j++) {
clickNumber.clickLongAccumulator();
}
} finally {
countDownLatch4.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime-startTime)+"毫秒 \t clickLongAccumulator:"+clickNumber.longAccumulator.get());
}
效果图:
从效果图中看,LongAdder高并发大数据量下性能比AtomicLong好10倍
那么为什么LongAdder能那么快呢?
LongAdder 高性能原理说明
AtomicLong是通过CAS自旋保证原子性,高并发大数量的情况下,也是一个个执行,所以性能不佳
LongAdder底层有base跟cell理念设计,在低并发情况下,跟AtomicLong性质一样,通过本身base的CAS自旋处理,但是高并发情况下,base一个肯定忙不过来,就启用cell扩容,开启多个窗口cell[0]、cell[1]、cell[…],利用base+cell达到分散热点的作用,如果要获取真正的long值,结果就是base+cell数组
而以上LongAdder设计思想是通过Striped64这个类落地实现的,LongAdder是Striped64的子类
LongAdder性能那么好主要原因是Striped64,这是Striped64类的一些变量或参数定义,其中最重要的就是NCPU、cells、base、cellsBusy
- NCPU:当前计算机CPU数量,即cells数组的最大长度,Cell数组扩容时会使用到
- cells:cell数组,为二次方的扩容,2、4、8…
- base:类似于AtomicLong中全局的value值。在没有竟争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
- cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态 1:表示其他线程已经持有了锁
- collide:表示扩容意向,false一定不会扩容,true可能会扩容
- casCelsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
- getRrobe():获取当前线程的hash值
- advanceProbe():重置当前线程的hash值
LongAdder源码解析
源码分析按照以下链路来看,其中longAccumulate是重点
LongAdder.increment()->
LongAdder.add()->
Striped64.longAccumulate()->
LongAdder.sum()
LongAdder.add
add方法总结:
- 最初无竞争时,只更新base
- 如果更新base失败后,初始化cell[]数组
- 当多个线程竞争同一个cell比较激烈时,可能就要对cell[]扩容
Striped64.longAccumulate
给每个cell槽位进行hash编号
这个大for cas自旋,主要做了这三个事情:
- cell数组已经初始化
- cell数据未初始化(首次新建)
- cell数组正在初始化中
LongAdder.sum()
sum()会将所有Cell数组中的value和base累加作为返回值。
核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
为什么并发情况下sum不精准呢?
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base己经被更新了,而此时局部变量sum不会更新,造成不一致。
其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
LongAdder小总结
AtomicLong:线程安全,可允许一些性能损耗,要求高精度时可使用,保证精度,性能代价;是多个线程针对单个热点值value进行原子操作
原理:CAS+自旋(incrementAndGet)
场景:低并发下的全局计算,Atomicong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。
缺陷:高并发后性能急剧下降,为什么?因为AtomicLong的自旋会成为瓶颈,高并发后造成大量cpu空转
LongAdder:当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用,保证性能,用精度付出代价;是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
原理:CAS+Base+Cell数组分散,空间换时间并分散了热点数据
场景:高并发下的全局计算
缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确
相关文献
就先说到这
\color{#008B8B}{ 就先说到这}
就先说到这
在下
A
p
o
l
l
o
\color{#008B8B}{在下Apollo}
在下Apollo
一个爱分享
J
a
v
a
、生活的小人物,
\color{#008B8B}{一个爱分享Java、生活的小人物,}
一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞!
\color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!}
咱们来日方长,有缘江湖再见,告辞!