下面所写,完全是下面的链接里视频说讲的笔记
原子类之基本类型原子类
原子类是指java.util.concurrent.atomic 包下的18个原子操作类。他们天生支持原子操作,底层依赖CAS,不用加synchronized这样的重锁也能保证原子安全性的结果。
对功能进行分类:
- 再进行分类
1、AtomicBoolean AtomicInteger AtomicIntegerArray AtomicIntegerFieldUpdater AtomicLong AtomicLongArray AtomicLongFieldUpdater AtomicMarkableReference AtomicReference AtomicReferenceArray AtomicReferenceFieldUpdater AtomicStampedReference
2、DoubleAccumulator DoubleAdder LongAccunmulator LongAdder
代码的例子在com.pbp.thread.atomic包下
- 基本类型原子类:AtomicInteger , AtomicBoolean , AtomicLong
- 数据类型原子类 :AtomicIntegerArray, AtomicLongArray , AtomicReferenceArray
- 应用类型原子类 : AtomicReference :自旋锁 SpinLockDemo ,
AtomicStampedReference ,携带版本号的引用类型原子类,可以解决ABA问题。主要解决修改过多少次
AtomicMarkableReference :与AtomicStampedReference 类似,但是只记录有没有修改过,主要解决是否修改过。而 AtomicStampedReference 可以记录修改过多少次。 - 对象的属性修改原子类: AtomicIntegerFieldUpdater: ,原子更新对象中int类型字段值
AtomicLongFieldUpdater ,原子更新对象long类型字段值
AtomicReferenceFieldUpdater ,原子更新引用类型字段值
作用:以一种线程安全的方式操作非线程安全对象内的某些字段
使用要求:更新的对象属性必须使用public volitile修饰
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
以前要线程安全时的做法是需要加synchronized关键字锁住整个对象
class Bank {
String bankName = "CCB";
int money = 0;
//利用synchronized锁住
public synchronized void add(){
money++;
}
}
public class AtomicIntegerReferenceUpdaterTest {
public static void main(String[] args) throws InterruptedException {
/* AtomicIntegerFieldUpdater<bank> bankUpdater =
AtomicIntegerFieldUpdater.newUpdater(bank.class, "money");*/
CountDownLatch countDownLatch = new CountDownLatch(10);
Bank bank = new Bank();
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
bank.add();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+":"+bank.money);
}
}
看看现在用AtomicReferenceFieldUpdater
class Bank {
String bankName = "CCB";
volatile int money = 0;
AtomicIntegerFieldUpdater<Bank> bankUpdater =
AtomicIntegerFieldUpdater.newUpdater(Bank.class, "money");
public void transMoney(Bank bank){
bankUpdater.incrementAndGet(bank);
}
}
public class AtomicIntegerReferenceUpdaterTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
Bank bank = new Bank();
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
for (int i1 = 0; i1 < 1000; i1++) {
bank.transMoney(bank);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+":"+bank.money);
}
}
在使用了AtomicReferenceFieldUpdater后,发现确实和synchronized关键字呈现的效果是一样的,但是它有一个局限性,就是只能对volatile int类型的字段进行原子更新,别的类型不可以。那么如果要更新别的类型的字段呢?接下来我们看AtomicReferenceFiledUpdater<T,V>,指定的volatile字段进行原子更新
例子:
class MyVal{//资源类
public volatile Boolean isInit = Boolean.FALSE;
//利用它提供的静态方法newUpdater来获取对象,可以看文档
AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVal.class,Boolean.class,"isInit");
public void init(MyVal myVal){
if(referenceFieldUpdater.compareAndSet(myVal,false,true)){
System.out.println(Thread.currentThread().getName()+":开始初始化");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":初始化结束");
}else{
System.out.println(Thread.currentThread().getName()+":无法初始化,因为已被其他线程初始化");
}
}
}
public class AtomicReferenceFieldUpdaterTest {
public static final int count = 10;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(count);
CountDownLatch countDownLatch = new CountDownLatch(count);
MyVal myVal = new MyVal();
for (int i = 0; i < count; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
myVal.init(myVal);
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束");
}
}
上面利用AtomicReferenceFieldUpdater对资源类进行了微创小手术,只改变对象的某个属性时只锁定对应的属性就好了,不用使用synchronized锁住整个对象
- 原子操作增强类原理深度解析
DoubleAccumulator、 DoubleAdder 、LongAccunmulator、 LongAdder
如果是JDK8的话,推荐使用LongAdde对象,比AtomicLong性能更好(减少乐观锁的重试次数)
题目举例:
- 热点商品点赞计数器,点赞数加加操作,不需要实时精确
- 一个很大的List,里面都是int类型,如何实现加加操作,说说思路
入门讲解:
- LongAdder只能计算加法,且从0开始
- LongAccumulator提供了自定义的函数操作
下面是LongAccumulator的例子:
public class LongAccumulatorTest {
public static void main(String[] args) {
LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
return left+right;//left是初始值,right是传进来的值
}
},1);
longAccumulator.accumulate(2);
System.out.println(longAccumulator.get());
}
}
LongAdder高性能对比示例:高性能统计点赞数
class ClickNumber{
public int number = 0;
public synchronized void add(){
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void addByAtomicLong(){
atomicLong.incrementAndGet();
}
LongAdder longAdder = new LongAdder();
public void addByLongAdder(){
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x,y)-> x+y,0);
public void addByAccumulator(){
longAccumulator.accumulate(1);
}
}
public class AccumulateCompareDemo {
public static int _1W = 10000;
public static int threadNum = 50;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);
CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);
CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);
CountDownLatch countDownLatch4 = new CountDownLatch(threadNum);
ClickNumber clickNumber = new ClickNumber();
Long startTime;
Long endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try{
for (int i1 = 1; i1 <= _1W * 100; i1++) {
clickNumber.add();
}
}finally {
countDownLatch1.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("消耗了:"+(endTime-startTime));
System.out.println("synchronized完成,总数是:"+clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try{
for (int i1 = 1; i1 <= _1W * 100; i1++) {
clickNumber.addByAtomicLong();
}
}finally {
countDownLatch2.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("消耗了:"+(endTime-startTime));
System.out.println("addByAtomicLong完成,总数是:"+clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try{
for (int i1 = 1; i1 <= _1W * 100; i1++) {
clickNumber.addByLongAdder();
}
}finally {
countDownLatch3.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("消耗了:"+(endTime-startTime));
System.out.println("addByLongAdder完成,总数是:"+clickNumber.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try{
for (int i1 = 1; i1 <= _1W * 100; i1++) {
clickNumber.addByAccumulator();
}
}finally {
countDownLatch4.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("消耗了:"+(endTime-startTime));
System.out.println("addByAccumulator完成,总数是:"+clickNumber.longAccumulator.get());
}
}
输出:
消耗了:1039
synchronized完成,总数是:50000000
消耗了:581
addByAtomicLong完成,总数是:50000000
消耗了:73
addByLongAdder完成,总数是:50000000
消耗了:40
addByAccumulator完成,总数是:50000000
可以看到LongAdder和LongAccumulator的效率比前面2个的高上不少,那么它们高效的原因我们分析一下:
AtomicLong:
这个原子类的底层是利用CAS,CAS只允许一个线程操作共享变量,其他线程会在外面自旋。
如果是低并发量,数据量少的情况下当然没什么问题。但是量变会引发质变。像上面的例子,几十个线程在外面自旋,CPU的占用一下就会升高
LongAdder:
化整为零,分散热点。
底层有个属性:long base和Cell[] cells,在低并发的时候和AtomicLong一样只有一个base,当在高并发时,LongAdder会分散热点,在数组里面创建多个节点给其他线程进行操作。
例子:假设在火车站买票只有一个网点,如果现在人少来买票的话AtomicLong和LongAdder是一样的,只有一个网点,大家都在这个网点排队。当人多起来的时候,AtomicLong还是只有一个网点,只能默默承受。而LongAdder可以创建多个网点供人们买票,这就是LongAdder性能高的原因。——分散热点
总结:LongAdder的基本思路就是分散热点,将value的值分散到一个Cell数组中,不同的线程会命中到对应的数组的不同的槽中,各个线程只会对自己槽中的value进行CAS操作,这样就把热点给分散了,冲突的概率就会小很多。如果要获得真正的long值,只要将槽中的value加起来返回。
sum()会将Sell和base的值相加返回,核心思想就是将AtomicLong的value分散到多个value中,从而降级更新热点
LongAdder为什么那么快?(源码解读)
**LongAdder的继承关系:**继承与Striped64
对于Striped64,里面有几个比较关键的属性:
Striped64里相关属性介绍:
主要思路就是:低并发是,LongAdder和LongAtomic的机制是一样的,但是高并发时,LongAdder会进行分散热点。
源码分析
知道LongAdder为什么那么快,开始分析源码。看源码之前,我们先看总结:
总结:LongAdder在无竞争的时候,跟AtomicLong一样,对同一个base进行操作。当出现高竞争的时候,则采用化整为零分散热点的方法,用空间换时间,用一个数组cells,将一个value拆分到这个cells里。多个线程需要同时对value进行操作时,可以根据线程id进行hash运算得到hash值,再根据哈希值映射到数组的某个下标,再对该下标对应的值进行自增操作。当所有线程完成后,将数组cells的所有值和base值求和得到最终结果。
现在以longAdder.increment()为例,基础方法有三个
- add():base的相加,数组的扩容
- longAccumulate:主要干活的方法
- sum:求和
//as为cell数组的引用
//b为base值
//v为当前线程定位到的cell的value
//m为cells长度-1,用于hash时作掩码使用
//a为当前线程命中的cell单元格
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//判断1
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
//判断2
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);
}
}
- (低并发)判断1:当只有一个线程进入的时候,判断:(as = cells) != null 为false,因为此时数组还没有初始化。接下来看第二个判断条件:这里是调用cas进行自增,在低并发时修改值成功,再取反为false,所以直接跳过下面的代码。
现在有多个线程进来了。
- (高并发)判断1:在竞争激烈的时候,对于判断1的分析如下:同样的,判断:(as = cells) != null 为false,因为此时还没有初始化数组。在第二个判断时,由于竞争激烈,cas修改值失败,所以开始执行longAccumulate()。
进入到longAccumulate(),里面有一个 for (;😉 ,在for里面有一个初始化的代码段
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;
}
- 在没有cell数组时,我们默认新建的cell数组的长度是2。初始化完数组之后赋值init = true,后面直接break跳出
继续到add()方法
如今判断1的(as = cells) != null 已经为true,我们要看判断2
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 == null || (m = as.length - 1) < 0 已经不满足了,因为数组已经初始化
(a = as[getProbe() & m]) == null :这里getProbe() & m 相当于是进行hash运算得到某个槽位的地址,并赋值给a,然后判断这个槽位有没有值,没有的话说明当前线程所在的cell为空,说明当前线程还没有更新过cell,应该初始化一个cell
!(uncontended = a.cas(v = a.value, v + x)):a数组单元格进行cas操作 ,a.cas(比较值, 期望值)。完成之后,将结果赋值给uncontended(无冲突),如果赋值成功直接跳出判断。
在并发上来之后,多个线程对同一个槽位进行cas操作时,cas失败了,那么就开始进入longAccumulate扩容了
初步总结:
- 最初没有竞争的时候只更新base
- 如果更新base失败后,首次新建一个cell数组
- 多个线程竞争同一个cell比较激烈的时候(怎么说比较激烈?在操作同一个cell时cas失败),对cell数组进行扩容
一些变量或者方法的定义
longAccumulate的详解:
进入longAccumulate的第一件事是判断:h = getProbe()) == 0 ,这的意义是判断当前的线程有没有对应的哈希值,没有哈希值怎么知道需要进入的是哪个槽位。就像是新入职的员工领取工号一样,没有的话要初始化一个。
if ((h = getProbe()) == 0) {
//使用ThreadLocalRandom为当前线程重新计算一个哈希值,强制初始化
ThreadLocalRandom.current(); // force initialization
//重新获取Probe值,重新获取哈希值就相当于一个全新的线程一样
h = getProbe();
//既然没有初始化,那么竞争就还不算太激烈,所以将wasUncontended 重新设置为true
wasUncontended = true;
}
接下来继续往下走,分为三部分
//1、cells已经被初始化
if ((as = cells) != null && (n = as.length) > 0){...}
//2、初始化一个cells数组
else if (cellsBusy == 0 && cells == as && casCellsBusy()){...}
//3、
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
第二部分:初始化一个cells数组
//每次操作这个数组要加锁,cellsBusy == 0 则为无锁状态。casCellsBusy是通过cas来获取锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2]; //初始化一个长度为2的数组
rs[h & 1] = new Cell(x);// 将x值赋值到某一个曹伟中
cells = rs;
init = true; //初始化成功
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
第三部分:兜底:多个线程尝试CAS修改失败时会走到这个分支
//直接操作base基数,将值累加到base上。即其他线程正在初始化数组,多个线程正在更新base值
else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
break; // Fall back on using base
第一部分:cells数组不再为空并且可能需要扩容
分为六部分
下面代码判断线程hash后指向数据的位置元素是否为空
如果为空就将值直接放入数组,跳出循环
不为空则继续循环
if ((as = cells) != null && (n = as.length) > 0) {
//数组对应的槽位没有值
if ((a = as[(n - 1) & h]) == null) {
//尝试去获取锁
if (cellsBusy == 0) {
Cell r = new Cell(x);
//double check
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try {
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;
}
wasUncontended表示数组初始化后,线程竞争失败了,传进来的时候是false,现在要取反,则表示重新竞争
//重新竞争
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
说明当前线程对应的数组已经有值,也充值过hash值
这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则跳出循环
//将值写入对应的槽位
else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
break;
这里判断数组长度是否超过CPU核数,超过的话则不需要扩容了
并且通过下面的h = advanceProbe(h);重置hash值,再次重新竞争
//判断是否需要扩容
else if (n >= NCPU || cells != as)
collide = false;
这里设置允许扩容标志
//取反,允许扩容
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
}
重置当前线程的hash值
h = advanceProbe(h);
总结图:
sum() 求和
sum执行时,被没有限制对base和Cell[]的更新。所以LongAdder不是强一致性的而是最终一致性的
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;
}
最后比较
AtomicLong:
线程安全,可允许一些性能的损耗,要求高精度时可用
保证精度,性能代价
AtomicLong是针对多个线程对单个热点进行原子操作
LongAdder:
当需要在高并发时有较好的表现,并且对精确度要求不高的情况下使用
保证性能,精度代价
LongAdder是每个线程都有对应的槽位,各个线程只会对自己对应的槽中的那个值进行CAS操作