原子操作类

在这里插入图片描述

在这里插入图片描述

分组来给大家讲解相关原子类的常用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方法总结:

  1. 最初无竞争时,只更新base
  2. 如果更新base失败后,初始化cell[]数组
  3. 当多个线程竞争同一个cell比较激烈时,可能就要对cell[]扩容

Striped64.longAccumulate

给每个cell槽位进行hash编号
在这里插入图片描述
这个大for cas自旋,主要做了这三个事情:

  1. cell数组已经初始化
  2. cell数据未初始化(首次新建)
  3. 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求和后还有计算线程修改结果的话,最后结果不够准确

相关文献

jdk api文档:https://www.runoob.com/manual/jdk11api/java.base/java/util/concurrent/atomic/package-summary.html

就先说到这 \color{#008B8B}{ 就先说到这} 就先说到这
在下 A p o l l o \color{#008B8B}{在下Apollo} 在下Apollo
一个爱分享 J a v a 、生活的小人物, \color{#008B8B}{一个爱分享Java、生活的小人物,} 一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞! \color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!} 咱们来日方长,有缘江湖再见,告辞!

在这里插入图片描述

### 作用与使用方法 #### 概念与作用 原子操作类如 `AtomicInteger` 是 Java 并发编程中用于保证共享变量操作原子性的工具。在多线程环境下,普通的变量操作(如自增、赋值)可能不是线程安全的,导致数据竞争问题。`AtomicInteger` 通过硬件级别的原子操作指令(如 CAS,Compare-And-Swap)来确保操作的原子性,从而避免了使用锁的性能开销。这使得它在高并发场景中具有更高的效率和更好的可扩展性[^2]。 #### 使用方法 `AtomicInteger` 提供了多种方法用于操作整型变量,这些方法包括但不限于: - `int get()`:获取当前值。 - `void set(int newValue)`:设置新值。 - `boolean compareAndSet(int expect, int update)`:比较并交换值,只有当前值等于 `expect` 时才更新为 `update`。 - `int getAndIncrement()`:获取当前值后自增。 - `int incrementAndGet()`:自增后获取新值。 - `int addAndGet(int delta)`:加上指定增量后获取新值。 以下是一个简单的示例,展示了 `AtomicInteger` 在并发环境中的使用: ```java import java.util.concurrent.atomic.AtomicInteger; public class Producer implements Runnable { private static AtomicInteger count = new AtomicInteger(); public void run() { String data = null; count.incrementAndGet(); data = "data:" + count.incrementAndGet(); System.out.println("将数据:" + data + "放入队列..."); } } ``` 在这个例子中,多个线程通过调用 `count.incrementAndGet()` 方法来确保对共享变量 `count` 的操作是线程安全的。这种方法避免了使用锁机制,同时保证了操作的原子性[^5]。 #### 底层实现原理 `AtomicInteger` 的实现依赖于 `Unsafe` 类,它提供了底层的硬件级原子操作。`Unsafe` 类通过 CAS 操作(Compare-And-Swap)来实现变量的原子更新。例如,`AtomicInteger` 中的 `incrementAndGet()` 方法实际上调用了 `Unsafe` 的 CAS 操作来确保自增过程的原子性。以下是一个简化的 `AtomicInteger` 类定义: ```java private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; private volatile int value; public AtomicInteger(int initialValue) { value = initialValue; } public AtomicInteger() { } ``` 其中,`valueOffset` 是变量 `value` 在对象内存中的偏移地址,`Unsafe` 利用这个偏移地址来直接操作内存中的变量值,从而实现原子操作[^1]。 #### 函数式编程支持 `AtomicInteger` 还支持以函数式编程的方式更新值。例如,`accumulateAndGet` 方法允许传入一个二元操作函数,用于更新当前值。以下是一个示例: ```java import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(5); atomicInt.accumulateAndGet(3, (x, y) -> x * y); // 5 * 3 = 15 System.out.println(atomicInt.get()); // 输出 15 } } ``` 在这个示例中,`accumulateAndGet` 方法通过传入的函数 `(x, y) -> x * y` 更新当前值,这种设计使得 `AtomicInteger` 在某些场景下更加灵活[^4]。 --- ### 相关问题 1. `AtomicInteger` 在高并发环境下如何避免锁的性能开销? 2. 如何利用 `AtomicInteger` 实现线程安全的计数器? 3. `AtomicInteger` 的 `compareAndSet` 方法如何保证操作的原子性? 4. `AtomicInteger` 与 `volatile` 变量在并发编程中的区别是什么? 5. 在哪些场景下更适合使用 `AtomicInteger` 而不是传统的锁机制?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值