1、是什么
2、基本类型原子类
2.1、AtomicInteger
2.2、AtomicBoolean
2.2、AtomicLong
2.4、API
public final int get()//获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement()//获取当前的值,并自减
public final int getAndAdd(int delta)//获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update)//比较并交换
2.5、Case
public class AtomicIntegerDemo {
public static final int SIZE = 50;
public static void main(String[] args) throws InterruptedException {
MyNumber myNumber = new MyNumber();
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for (int i = 0; i < SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
myNumber.addPlusPlus();
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
//等待上面50个线程全部计算完成后,再去获得最终值
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + myNumber.atomicInteger.get());
}
}
class MyNumber {
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus() {
atomicInteger.getAndIncrement();
}
}
3、数组类型原子类
3.1、AtomicIntegerArray
3.2、AtomicLongArray
3.3、AtomicReferenceArray
3.4、Case
4、引用类型原子类
4.1、AtomicReference
自旋锁SpinLockDemo
可查看 6.8.1、手写
4.2、AtomicStampedReference
携带版本号的引用类型原子类,可以解决ABA
问题
解决修改过多少次
可查看 6.9.2.2.1
4.3、AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过
它的定义就是将状态戳简化为 true l false
类似一次性筷子
public class AtomicMarkableReferenceDemo {
static AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);
public static void main(String[] args) {
new Thread(() -> {
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t默认标识" + marked);
//等待后面的t2线程和我拿到一样的模式flag标识,都是false
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
markableReference.compareAndSet(100, 1000, marked, !marked);
}, "t1").start();
new Thread(() -> {
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t默认标识" + marked);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = markableReference.compareAndSet(100, 2000, marked, !marked);
System.out.println(Thread.currentThread().getName() + "\t" + "t2线程CASresult:" + b);
System.out.println(Thread.currentThread().getName() + "\t" + markableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t" + markableReference.getReference());
}, "t2").start();
}
}
5、对象的属性修改原子类
5.1、AtomicIntegerFieldUpdater
原子更新对象中int
类型字段的值
基于反射的实用程序,可对指定类的指定volatile int
字段进行原子更新
5.2、AtomicLongFieldUpdater
原子更新对象中Long
类型字段的值
5.3、AtomicReferenceFieldUpdater
原子更新对象引用类型字段的值
5.4、使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段
5.5、使用要求
更新的对象属性必须使用public volatile
修饰符。
因为对象的属性修改类型原子类都是抽象类,
- 所以每次使用都必须使用静态方法
newUpdater()
创建一个更新器,并且需要设置想要更新的类和属性。
5.6、你在哪使用过volatile?
上述三者都可以
5.7、Case
AtomicIntegerFieldUpdater
/**
* 以一种线程安全的方式操作非线程安全对象的某些字段。
* 需求:
* 10个线程,
* 每个线程转账1000,
* 不使用synchronized,尝试使用AtomicIntegerFieldupdater来实现。
*/
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
// bankAccount.add();
bankAccount.transMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + bankAccount.money);
}
}
class BankAccount {
String bankName = "CCB";
//更新对象属性必须使用 public volatile 修饰符
volatile int money = 0;
public void add() {
money++;
}
//因为对象的属性修改类型原子类都是抽象类,
//所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
//不加synchronized
public void transMoney(BankAccount bankAccount){
fieldUpdater.getAndIncrement(bankAccount);
}
}
AtomicReferenceFieldUpdater
/**
* 需求:
* 多线程并发调用一个类的初始化方法,I如果未被初始化过,将执行初始化工作,
* 要求只能被初始化一次,只有一个线程操作成功
*/
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 0; i < 9; i++) {
new Thread(()->{
myVar.init(myVar);
},String.valueOf(i)).start();
}
}
}
class MyVar {
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
public void init(MyVar myVar){
if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)) {
System.out.println(Thread.currentThread().getName()+"\t----start init");
System.out.println(Thread.currentThread().getName()+"\t----over init");
}else {
System.out.println(Thread.currentThread().getName()+"\t -----已经有现成再进行初始化工作");
}
}
}
6、原子操作增强类原理深度解析!!!
当多个线程更新用于收集统计信息但不用于细拉度同步控制的目的的公共和时,此类通常优于AtomicLong
。
在低更新争用下,这两个类具有相似的特征。但在高争用的情况下,这一类的预期吐量明显更高,但代价是空间消耗更高。
6.1、DoubleAccumulator
.6.2、DoubleAdder
6.3、LongAccumulator
6.4、LongAdder
6.5、面试题
6.6、点赞计数器
6.6.1、常用API
6.6.2、入门讲解
LongAdder
只能用来计算加法。且从零开始计算
LongAccumulator
提供了自定义的函数操作
Demo
public class LongAdderDemo {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());//3
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y,0);
longAccumulator.accumulate(1);//1 x=0 y=1 x+y=1
longAccumulator.accumulate(3);//4 x=1 y=3 x+y=4
System.out.println(longAccumulator.get()); // 4
}
}
6.6.3、LongAdder高性能对比Code演示
package com.atomics;
/**
* @PACKAGE_NAME: com.atomics
* @NAME: AccumulatorCompareDemo
* @USER: Mrs.Wang
* @DATE: 2022/9/25
* @TIME: 17:23
* @DAY_NAME_SHORT: 周日
* @DAY_NAME_FULL: 星期日
* @PROJECT_NAME: JUC
**/
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
/**
* 50个线程,每个线程100w从,总点赞出来
*/
public class AccumulatorCompareDemo {
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;
CountDownLatch countDownLatch = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for (int i = 0; i <= threadNumber; i++) {
new Thread(()->{
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByLongAccumulator();
}
}finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime:" + (endTime - startTime) + "ms");
}
}
class ClickNumber {
int number = 0;
//----costTime clickBySynchronized :2217ms
public synchronized void clickBySynchronized() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
//----costTime: clickByAtomicLong : 1163ms
public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
//----costTime: clickByLongAdder : 138ms
public void clickByLongAdder() {
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
//----costTime: clickByLongAccumulator : 140ms
public voiad clickByLongAccumulator(){
longAccumulator.accumulate(1);
}
}
6.7、源码、原理分析
6.7.1、架构
6.7.2、LongAdder是Striped64的子类
6.7.3、Striped64
6.7.4、Cell
是Striped64
的一个内部类
6.7.5、LongAddr为什么这么快
一句话
LongAdder
的基本思路就是分散热点,将value
值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS
操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long
值,只要将各个槽中的变量值累加返回。
sum()
会将所有Cell
数组中的value
和base
累加作为返回值,
核心的思想就是将之前AtomicLong
一个value
的更新压力分散到多个value
中去,从而降级更新热点。
内部有一个base
变量,一个 Cell[]
数组。
base
变量:
- 低并发,直接累加到该变量上
Cell[]
数组:
- 高并发,累加进各个线程自己的槽cell[i]中
6.7.6、源码解读深度分析
6.7.6.1、小总结
LongAdder
在无竞争的情况,跟AtomicLong
一样,对同一个base
进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,
用一个数组cells
,将一个value
拆分进这个数组cells
。
多个线程需要同时对value
进行操作时候,可以对线程id
进行hash
得到hash值
,再根据hash值
映射到这个数组cell
的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells
的所有值和base
都加起来作为最终结果。
6.7.6.2、longAddr.increment()
6.7.6.2.1、add(1L)
- 最初无竞争时只更新
base
; - 如果更新
base
失败后,首次新建一个Cell[]
数组 - 当多个线程竞争同一个
Cell
比较激烈时,可能要对Cell[]
数组扩容
public void add(long x) {
/**
as是striped64中的cells数组属性
b是Striped64中的base属性
v是当前线程hash到的Cell中存储的值
m是cells的长度减1, hash时作为掩码使用
a是当前线程hash到的cell
*/
Cell[] as; long b, v; int m; Cell a;
//首次首线程((as = cells) != null)一定是false,此时走casBase方法,以CAS的方式更base值,且只有当cas失败时,才会走到if中
//条件1: cells不为空
//条件2:cas操作base失败,说明其他线程先一步修改了base正在出现竞争
if ((as = cells) != null || !casBase(b = base, b + x)) {
//true 无竞争 false表示竞争激烈,多个线程hash到同一个Cell,可能要扩容
boolean uncontended = true;
//条件1:cells为空
//条件2:应该不会出现
//条件3:当前线程所在的Cell为空。说明当前线程还没有更新过Cell,应初始化一个Cell
//条件4:更新当前线程所在的Cell失败,说明现在竞争很激烈,,多个线程hash到同一个Cell,应扩容
if (as == null || (m = as.length - 1) < 0 ||
//getProbe()方法返回的是线程中的threadLocalRandomProbe字段
//它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非刻意修改它)
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
-
如果
cells
表为空,尝试用CAS
更新base
字段,成功则退出; -
如果
Cells
表为空,CAS
更新base
字段失败,出现竞争(线程多),uncontended
为true
,调用longAccumulate
; -
如果
Cells
表非空,但当前线程映射的槽为空,uncontended
为true
,调用longAccumulate
; -
如果
Cells
表非空,且前线程映射的槽非空,CAS
更新Cell
的值,成功则返回,否则(线程更多),uncontended
设为false
,调用longAccumulate
。
6.7.6.2.2、longAccumulate
longAccumulate入参说明
步骤
- 线程
hash
值:probe
类似于槽位的下标
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//存储线程的probe值
int h;
//如果getProbe()方法返回0,说明随机数为初始化
if ((h = getProbe()) == 0) {
//使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
ThreadLocalRandom.current(); // force initialization
//重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true
h = getProbe();
//重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈wasUncontended竞争状态为true
wasUncontended = true;
}
...
}
- 总纲
CASE1: Cell[]数组已经初始化 3
CASE2: Cell[]数组未初始化(首次新建) 1
CASE3: Cell[]数组正在初始化中 2 尝试直接在base
上累加操作,成功返回。否则重置hash
循环
- 计算
刚刚要初始化Cell[]数组(首次新建) CASE2: Cell[]
数组未初始化(首次新建)
未初始化过Cell[]
数组,尝试占有锁并首次初始化cells
数组
如果上面条件都执行成功就会执行数组的初始化及赋值操作,Cell[] rs = new Cel[2]
表示数组的长度为2
,rs[h&1]=new Cell(x)
表示创建一个新的Cell
元素,value
是x
值,默认为1
。
h&1
类似于我们之前HashMap
常用到的计算散列桶index
的算法,通常都是hash &(table.len-1)
。同hashmap
一个意思。
对于dc
假如有两个线程同时走到这销断里面,第一个线程判断cellsBusy == o && cells == as
(as
为空,cells
还没初始化也为空)都是true
,此时线程一时间片用完。
线程二进来,它他们判断cellsBusy == o && cells == as
都为true
。并且casCellBusy()
成功,此时进入if
时成功的代玛块。并且执行完毕把cellsBusy
变为了0
,
此时cells
已经初始化,线程二时间片用完。线程一接着执行casCellBusy()
并且成功。它也进if
成功的代码块。此时try
中的if(cellls==as)
就为false
,防止了多次初始化
兜底
多个线程尝试CAS
修改失败的线程会走到这个分支
该分支实现直接操作base
基数,将值累加到base
上,也即其它线程正在初始化,多个线程正在更新base
的值。
Cell数组不再为空且可能存在Cell数组扩容
多个线程同时命中一个cell
的竞争
1、对初始化的Cell
数组进行赋值操作
//CASE1:cells已经被初始化了
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {//当前线程的hash运算后映射的Cell单元为null,说明Cell没有被使用
if (cellsBusy == 0) { // Try to attach new Cell ,Cell[]数组没有正在扩容,就是锁
Cell r = new Cell(x); // Optimistically create 创建一个Cell单元
if (cellsBusy == 0 && casCellsBusy()) {//尝试加锁,成功后cellsBusy==1,双重检查,防止正在扩容
boolean created = false;
//-------------
try { // Recheck under lock 在有锁的情况下再检测一遍之前的判断
Cell[] rs; int m, j;//将Cell单元附到Cell[]数组上
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;
}
上面代码判断当前线程hash
后指向的数据位置元素是否为空,
如果为空则将Cell
数据放入数组中,跳出循环。
如果不空则继续循环。
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
...
h = advanceProbe(h);
wasUncontended
表示cells
初始化后,当前线程竞争修改失败wasUncontended = false
,
这里只是重新设置了这个值为true
,
紧接着执行advanceProbe(h)
重置当前线程的hash(找其他的槽位),重新循环
+else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
...
h = advanceProbe(h);
说明当前线程对应的数组中有了数据,也重置过hash
值,
这时通过CAS
操作尝试对当前数中的value值进行累加x
操作,x
默认为1
,如果CAS
成功则直接跳出循环。
失败的话,h = advanceProbe(h)
; 重置hash
值,找其他的槽位
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
...
h = advanceProbe(h);
如果n
大于CPU
最大数量,不可扩容,
并通过下面的h = advanceProbe(h)
方法修改线程的probe
再重新尝试
重置哈希
else if (!collide)
collide = true;
...
h = advanceProbe(h);
如果扩容意向collide
是false
则修改它为true
,然后重新计算当前线程的hash
值继续循环,
如果当前数组的长度已经大于了CPU
的核数,就会再次设置扩容意向collide=false
(见上一步)
扩容Cell数组,并且
总结
如果槽位为空,cas
失败,线程压力大,进入longAccumlate
方法,进行对Cell
数组初始化操作(双重检查进行初始化),
如果其他线程进来发现正在初始化,尝试直接对base cas + 1
操作,如果成功返回,失败重新计算hash值,找其他的槽位循环。
此时Cell
初始化两个数组成功了,继续循环Cell
不为空 Cell
槽位为空,进行赋值操作,其中也是双重检查加锁进行
赋值。成功了直接跳出循环返回。失败了的话重新循环。
如果竞争压力更大了,可能要进行扩容,首先竞争标志位为false
,改为true
。重新计算hash
值,找其他槽位进行循环操作,
如果当前槽位cas +1
成功,直接跳出循环返回。失败的话如果是当前Cell进行扩容操作 Cell
数组为2的次幂
,左移一位,否则循环。
6.7.6.2.3、sum()
sum()
会将所有Cell
数组中的value
和base
累加作为返回值。
核心的思想就是将之前AtomicLong
一个value
的更新压力分散到多个value
中去,从而降级更新热点。
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;
}
为啥在并发情况下sum的值不精确
sum
执行时,并没有限制对base
和cells
的更新(一句要命的话)。
所以LongAdder
不是强一致性的,它是最终一致性的。
首先,最终返回的sum
局部变量,初始被赋值为base
,而最终返回时,很可能base
已经被更新了,而此时局部变量sum
不会更新,造成不一致。
其次,这里对cell
的读取也无法保证是最后一次写入的值。所以,sum
方法在没有并发的情况下,可以获得正确的结果。
6.7.7、使用总结
6.7.7.1、AtomicLong(精度高,性能代价)
线程安全,可允许一些性能损耗,要求高精度时可使用
保证精度,性能代价
AtomicLong
是多个线程针对单个热点值value
进行原子操作
6.7.7.2、LongAddr(性能高,精度代价)
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder
是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS
操作
6.8、小总结
6.8.1、AtomicLong
6.8.1.1、原理
CAS+自旋
incrementAndGet
.8.1.2、场景
低并发下的全局计算
AtomicLong
能保证并发情况下计数的准确性,其内部通过CAS
来解决并发安全性的问题。
6.8.1.3、缺陷
高并发后性能急剧下降why?
AtomicLong
的自旋会成为瓶颈
N
个线程CAS
操作修改线程的值,每次只有一个成功过,其它N-1
失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu
就打高了。
6.8.2、LongAddr
6.8.2.1、原理
CAS+Base+Cell
数组分散
空间换时间并分散了热点数据
6.8.2.2、场景
高并发下的全局计算
6.8.2.3、缺陷
sum求和后还有计算线程修改结果的话,最后结果不够准确