【尚硅谷周阳--JUC并发编程】【第九章--原子操作类之18罗汉增强】
一、是什么?
二、分类
1、基本类型原子类
1.1、基本类型原子类包含以下三个
- AtomicInteger
- AtomicBoolean
- AtomicLong
1.2、常用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);
// 如果输入的数值等于预期值,则以原子方式将值设置为输入值(update)
public final boolean compareAndSet(int expect, int update);
1.3、案例
- 使用线程休眠方式,但是这种方式肯定是不可靠的,仅用于测试
class MyNumber {
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus() {
atomicInteger.getAndIncrement();
}
}
public class AtomicIntegerDemo {
public static final int SIZE = 50;
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
for (int i = 0; i < SIZE; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myNumber.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 等待上面线程执行完
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + "result:" + myNumber.atomicInteger.get());
}
}
- 可以使用CountDownLatch来替换线程休眠,实现主线程等待其他线程执行完毕
class MyNumber {
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus() {
atomicInteger.getAndIncrement();
}
}
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();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "result:" + myNumber.atomicInteger.get());
}
}
2、数组类型原子类
2.1、数组类型原子类包含以下三个
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
2.2、案例
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
AtomicIntegerArray integerArray = new AtomicIntegerArray(5);
// AtomicIntegerArray integerArray = new AtomicIntegerArray(new int[5]);
// AtomicIntegerArray integerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
for (int i = 0; i < integerArray.length(); i++) {
System.out.println(integerArray.get(i));
}
System.out.println();
int tmpInt = 0;
tmpInt = integerArray.getAndSet(0, 2024);
System.out.println(tmpInt + "\t" + integerArray.get(0));
tmpInt = integerArray.getAndIncrement(0);
System.out.println(tmpInt + "\t" + integerArray.get(0));
}
}
3、引用类型原子类
3.1、引用类型原子类包含以下三个
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
3.2、案例
3.2.1、AtomicReference
/**
* 题目:实现一个自旋锁,复习CAS思想
* 自旋锁好处:循环比较获取没有类似wait的阻塞
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来后发现
* 当前有现成持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到
*
* @author 匍匐丶前行
* @since 2024/3/19 15:25
**/
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
System.out.println(Thread.currentThread().getName() + "\t" + "------拿到资源");
}
public void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t" + "------task over");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
spinLockDemo.unlock();
}, "A").start();
// 保证线程A先启动
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
spinLockDemo.unlock();
},"B").start();
}
}
3.2.2、AtomicStampedReference
- 携带版本号的引用类型原子类,可以解决ABA问题
- 解决修改过几次的问题(每次修改都会有流水记录)
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号:" + stamp);
// 暂停500毫秒,保证后面的t4线程初始化拿到的版本号和当前线程一致
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t" + "二次版本号:" + stampedReference.getStamp());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t" + "三次版本号:" + stampedReference.getStamp());
}, "t1").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号:" + stamp);
// 等待t1线程发生ABA问题
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
boolean b = stampedReference.compareAndSet(100, 2024, stamp, stampedReference.getStamp() + 1);
System.out.println(b + "\t" + Thread.currentThread().getName() + "\t" + "二次版本号:" + stampedReference.getStamp());
}, "t2").start();
}
}
3.2.3、AtomicMarkableReference
- 原子更新带有标记位的引用类型对象
- 解决是否修改过
- 它的定义就是将状态戳简化为true|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线程拿到相同的默认false值
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
markableReference.compareAndSet(100, 1000, marked, !marked);
}, "t1").start();
new Thread(() -> {
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t" + marked);
// 等待t1线程先执行完
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
boolean b = markableReference.compareAndSet(100, 2024, 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();
}
}
4、对象的属性修改原子类
4.1、对象的属性修改原子类包含以下三个
- AtomicIntegerFieldUpdater(原子更新对象中int类型字段的值)
- AtomicLongFieldUpdater(原子更新对象中Long类型字段的值)
- AtomicRefrenceFieldUpdater(原子更新引用类型字段的值)
4.2、使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段
4.3、使用要求
- 更新的对象属性必须使用public volatile修饰符
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
4.4、在哪用过volatile关键字(面试)
一下三个对象的属性修改原子类,更新的对象属性必须使用public volatile修饰符
- AtomicIntegerFieldUpdater(原子更新对象中int类型字段的值)
- AtomicLongFieldUpdater(原子更新对象中Long类型字段的值)
- AtomicRefrenceFieldUpdater(原子更新引用类型字段的值)
4.5、案例
4.5.1、Integer类型
/**
* 以一种线程安全的方式操作非线程安全对象的某些字段
*
* 需求:
* 10个线程
* 每个线程 转账1000
* 不适用synchronized,尝试使用AtomicIntegerFieldUpdater来实现
*
* @author 匍匐丶前进
* @since 2024/3/21 11:03
**/
@NoArgsConstructor
@AllArgsConstructor
@Data
class BankAccount {
String bankName;
// 更新对象的属性必须要使用volatile修饰符
volatile int money;
}
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount("CCB", 0);
// 抽象类使用newUpdater创建议个更新器
AtomicIntegerFieldUpdater<BankAccount> integerFieldUpdater
= AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
integerFieldUpdater.getAndIncrement(bankAccount);
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + integerFieldUpdater.get(bankAccount));
}
}
4.5.2、Reference类型
/**
* 需求:
* 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作
* 要求只能被初始化一次,只有一个线程操作成功
*
* @author 匍匐丶前进
* @since 2024/3/21 13:27
**/
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, need 2 second");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + "------ task over");
} else {
System.out.println(Thread.currentThread().getName() + "\t" + "------ 已经有线程在进行初始化工作");
}
}
}
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
myVar.init(myVar);
}, String.valueOf(i)).start();
}
}
}
5、原子操作增强类原理深度解析
5.1、原子操作增强类包含以下四个
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
5.2、阿里巴巴开发手册及面试题说明
5.3、点赞计数器案例及性能比较
5.3.1、常用API
5.3.2、入门讲解
- LongAdder只能用来计算加法,且从零开始计算
- LongAccumulator提供了自定义的函数操作
public class LongAdderAPIDemo {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long x, long y) {
return x + y;
}
}, 0);
longAccumulator.accumulate(1);// x=0,y=1
longAccumulator.accumulate(3);// x=1,y=3
System.out.println(longAccumulator.longValue());
}
}
5.3.3、LongAdder高性能对比Code演示
/**
* 需求:50各线程,每个线程100w次,总点赞数出来
*
* @author 匍匐丶前进
* @since 2024/3/21 14:35
**/
class ClickNumber {
int number = 0;
public synchronized void clickBySynchronized() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}
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 class AccumulatorCompareDemo {
public static final int _100w = 1000000;
public static final int threadNumber = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 0; j < _100w; j++) {
clickNumber.clickBySynchronized();
}
} finally {
countDownLatch1.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime:" + (endTime - startTime) + "毫秒" + "\t clickBySynchronized:" + clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 0; j < _100w; j++) {
clickNumber.clickByAtomicLong();
}
} finally {
countDownLatch2.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime:" + (endTime - startTime) + "毫秒" + "\t clickByAtomicLong:" + clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 0; j < _100w; j++) {
clickNumber.clickLongAdder();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime:" + (endTime - startTime) + "毫秒" + "\t clickLongAdder:" + clickNumber.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 0; j < _100w; j++) {
clickNumber.clickLongAccumulator();
}
} finally {
countDownLatch4.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime:" + (endTime - startTime) + "毫秒" + "\t clickLongAccumulator:" + clickNumber.longAccumulator.get());
}
}
5.4、源码、原理分析
5.4.1、架构
5.4.2、原理(LongAdder为什么这么快)
- 官网说明和阿里要求
5.4.3、Striped64
- LongAdder是Striped64的子类
- Striped64下面有几个比较重要的成员函数
- 最重要的两个
- Striped64中一些变量或者方法的定义
5.4.4、Cell
是java.util.concurrent.atomic下Striped64的一个内部类
5.4.5、LongAdder为什么这么快
- LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个卡槽中的变量值累加返回。
- sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
- 总结
- 内部有一个base变量,一个cell[]数组。
- base变量:低并发,直接累加到该变量上
- Cell[]数组:高并发,累加进各个线程自己的槽Cell[i]中
- 数学公式
- 内部有一个base变量,一个cell[]数组。
5.5、源码解读深度分析
5.5.1、小总结
- LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同事对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果。
5.5.2、LongAdder.increment()为什么快(三步走)
5.5.2.1、add(1L)
- 最初无竞争时职只更新base
- 如果更新base失败后,首次新建一个Cell[]数组
- 当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
5.5.2.2、longAccumulate
- longAccumulate入参说明
- Striped64中一些变量或者方法的定义
- 步骤
- probe
- 总纲
上述代码首先给当前线程分配一个hash值没然进入一个fob(;;)自旋,这个自旋分为三个分支:
1、CASE2:Cell[]数组未初始化(首次创建)
2、CASE3:Cell[]数组正在初始化中
3、CASE1:Cell[]数组已经初始化- 多个线程同时命中一个cell的竞争
- 总体代码
- probe
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Striped64.Cell r = new Striped64.Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Striped64.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
Striped64.Cell[] rs = new Striped64.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);
}
总体流程图如下:
5.5.2.3、sum
- sum()会将所有cell数组中的value和base累加作为返回值
- 核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点
- 为什么在并发情况下sum的值不精确?
- sum在执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的
- 首次,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
/**
* Returns the current sum. The returned value is <em>NOT</em> an
* atomic snapshot; invocation in the absence of concurrent
* updates returns an accurate result, but concurrent updates that
* occur while the sum is being calculated might not be
* incorporated.
*
* @return the 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;
}
5.6、使用总结
5.6.1、AtomicLong
- 线程安全,可允许一些性能损耗,要求高精度时可使用
- 保证精度,性能代价
- AtomicLong是多个线程针对单个热点值value进行原子操作
5.6.2、LongAdder
- 当需要在高并发下有较好的性能表现,且对值得精度要求并不高时,可以使用
- 保证性能,精度代价
- LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
6、小总结
6.1、AtomicLong
- 原理
- CAS+自旋
- incrementAndGet
- 场景
- 低并发下的全局计算
- AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
- 缺陷
- 高并发后性能急剧下降
- AtomicLong的自旋会称为瓶颈
N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停的自旋知道成功,这样大量失败自旋的情况,一下子CPU就打高了
6.2、LongAdder
- 原理
- CAS+Base+Cell数组分散
- 空间换时间并分散了热点数据
- 场景
- 高并发下的全局计算
- 缺陷
- sum求和后还有计算线程修改结果的话,最后结果不够准确