AtomicLong与LongAdder对比

2024-04-23 03:48
文章标签 对比 longadder atomiclong

本文主要是介绍AtomicLong与LongAdder对比,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

《阿里巴巴 Java开发手册》读后感—拥抱规范,远离伤害:https://blog.csdn.net/f641385712/article/details/84930279

写这篇博文的原因,是因为我今天在看阿里的规范手册的时候(记录在了这里:《阿里巴巴 Java开发手册》读后感—拥抱规范,远离伤害),发现了有一句规范是这么写的:

如果是count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。

这里面提到了Atomic系列来进行原子操作。之前我在各个地方使用过AtomicInteger很多次,但一直没有做一个系统性的了解和做笔记。因此本此恰借此机会,把这块的知识点好好梳理一下, 并希望在学习的过程中解决掉问题

简单例子铺垫
废话不多说,展示代码:

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();

        Count count = new Count();

        // 100个线程对共享变量进行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.increase());
        }

        // 等待上述的线程执行完   和三个方法的区别 这里不做概述,反正都能关闭
        service.shutdown();
        //service.shutdownNow();
        service.awaitTermination(2, TimeUnit.SECONDS);
        service.shutdown();

        System.out.println(count.getCount());
    }

    //计数类
    private static class Count {
        // 共享变量
        private Integer count = 0;

        public Integer getCount() {
            return count;
        }

        public void increase() {
            count++;
        }
    }

你们猜猜执行的结果会是多少?是100吗?

我相信稍微基础好一点的,或者说遇见过类似问题的,答案都是No吧。我执行了多次,结果是不确定的:29、69、48、99都有。。。
(备注:类似的方案,有时候可以通过volatile关键字,此处不对此关键字做过多的讨论,它是一种内存可见性方案,并不是真正意义上的锁哟)

根据结果我们得知:上面的代码是线程不安全的!如果线程安全的代码,多次执行的结果是一致的!

原因分析
什么上述的结果不确定呢?我们可以发现问题所在:**count++并不是原子操作。**因为count++需要经过读取-修改-写入三个步骤。举个例子还原一下真相:

如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10
线程A对count++,此时count的值为11
线程B对count++,此时count的值也是11(因为线程B读到的count是10)
所以到这里应该知道为啥我们的结果是不确定了吧。
怎么破?
要得出正确的结果100,怎么办?

synchronized
在increase()加synchronized锁就好了:

public synchronized void increase() {
    count++;
}

这样子无论执行多少次,得出的都是100。这个对于只要求解决问题,但不在乎效率,不想深挖的人,肯定已经ok了。但是我们仅仅只是对于这么简单的一个++,就动用这么"强悍的"Synchronized未免有点太小题大作了。

Synchronized悲观锁,是独占的,意味着如果有别的线程在执行,当前线程只能是等待!

那么接下来针对我们频繁碰到这个问题,JDK5提供的原子操作就要登场了

Atomic原子操作
在JDK1.5+的版本中,Doug Lea和他的团队还为我们提供了一套用于保证线程安全的原子操作。

JDK1.5的版本中为我们提供了java.util.concurrent.atomic原子操作包。所谓“原子”操作,是指一组不可分割的操作:操作者对目标对象进行操作时,要么完成所有操作后其他操作者才能操作;要么这个操作者不能进行任何操作。

有了他们,我们就很好解决上面遇到的问题了,只需要采用AtomicInteger稍加改动就OK了~~

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();

        AtomicInteger count = new AtomicInteger();

        // 100个线程对共享变量进行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.incrementAndGet());
        }

        // 等待上述的线程执行完   和三个方法的区别 这里不做概述,反正都能关闭
        service.shutdown();
        //service.shutdownNow();
        service.awaitTermination(2, TimeUnit.SECONDS);
        service.shutdown();

        System.out.println(count.get());
    }

改用Atomic来执行后,我们发现不管执行多少次,结果都是正确的100;

JDK1.5以后这种轻量级的解决方案不再推荐使用synchronized,而使用Atomic代替,因为效率更高

源码分析
AotmicInteger其实就是对int的包装,然后里面内部使用CAS算法来保证操作的原子性

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }


可以看到,内部主要依赖于unsafe提供的CAS算法来实现的,因此我们很有必要了解一下,到底什么是CAS呢?

CAS解释
先概念走一波

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

从定义中我们可以总结出CAS有三个操作数:

内存值V
旧的预期值A
要修改的新值B
为了方便大家理解也为了我记忆深刻点,我特意自己尝试着画了一些图解(下同):

可以发现CAS有两种情况:

如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!
如果内存值V和我们的预期值A不相等,一般也有两种情况:
1、重试(自旋) 2、什么都不做
CAS失败重试(自旋)
上面的例子,我们启动的100个线程,实质上都对结果进行了+1。但是可以想象到,肯定存在多个线程同一时刻同时想+1的情况,因此可见下图:

虽然这幅图只画了两个线程的情况,举一反三,任意多个线程的情况都是一样的处理方式。

CAS失败—什么都不做
这个我就不再画图,说白了就是Z线程进来后,发现预期值和内存值不一样的时候,就什么都不做,就CAS失败,直接结束掉线程了。这个有些场景也会这么去干

CAS为什么是原子的呢?
有的人可能会问:CAS明明就有多部操作,但什么就是原子的呢?
解释如下:

Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作。这也就为啥CAS是原子性的,因为它是一条CPU指令,不会被打断。

CAS是原子性的,虽然你可能看到比较后再修改(compare and swap)觉得会有两个操作,但终究是原子性的!

CAS带来的ABA问题
什么是ABA问题呢?结束上面的例子

线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
此时线程A使用CAS将count值修改成100
修改完后,就在这时,线程B进来了(因为CPU随机,所以是有可能先执行B再执行C的),读取得到count的值为100(内存值和预期值都是100),将count值修改成10
线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成11
产生的问题是:线程C无法得知线程A和线程B修改过的count值,这样是有风险的。,如下:
场景:蛋糕店回馈客户,对于会员卡余额小于20的客户一次性赠送20,刺激消费,每个客户只能赠送一次

    public static void main(String[] args) {

         //在这里使用AtomicReference  里面装着用户的余额  初始卡余额小于20
        final AtomicReference<Integer> money = new AtomicReference<>(19);

        //模拟一个生产者消费者模型

        // 模拟多个线程更新数据库,为用户充值
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    while (true) {
                        Integer m = money.get();
                        if (m < 20) {
                            if (money.compareAndSet(m, m + 20)) {
                                System.out.println("余额小于20,充值成功。余额:"
                                        + money.get() + "元");
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值!");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        // 用户消费进程,模拟消费行为
        new Thread(() -> {
            //在这里的for循环,太快很容易看不到结果
            for (int i = 0; i < 1000; i++) {
                while (true) {
                    Integer m = money.get();

                    if (m > 10) {
                        System.out.println("大于10元");
                        if (money.compareAndSet(m, m - 10)) {
                            System.out.println("成功消费10,卡余额:" + money.get());
                            break;
                        }
                    } else {
                        System.out.println("余额不足!");
                        break;
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

输出:

余额小于20,充值成功。余额:39元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:29
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额小于20,充值成功。余额:29元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额不足!
余额大于20,无需充值!
余额大于20,无需充值!
余额小于20,充值成功。余额:29元

我们看到,这个帐号先后反复多次进行充值。,怎么回事呢?

原因是帐户余额被反复修改,修改后的值等于原来的值,使得CAS操作无法正确判断当前的数据状态。这在业务上是不允许的(只有高并发下才可能会出现哦,并不是说记录下赠送次数就能简单解决的哦)。

ABA问题如何解决
其实java也考虑到了这个问题,所以提供给予我们解决方案了

我们可以使用JDK给我们提供的AtomicStampedReference和AtomicMarkableReference类。

用代码解决上面的充值问题:该动起来也是非常的简单

   public static void main(String[] args) {

        //在这里使用AtomicReference  里面装着用户的余额  初始卡余额小于20
        final AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0);


        for (int i = 0; i < 3; i++) {
            //拿到当前的版本号
            final int timestamp = money.getStamp();


            new Thread(() -> {
                while (true) {
                    while (true) {
                        Integer m = money.getReference();
                        if (m < 20) {
                            //注意此处:timestamp版本号做了+1操作
                            if (money.compareAndSet(m, m + 20, timestamp,
                                    timestamp + 1)) {
                                System.out.println("余额小于20,充值成功。余额:"
                                        + money.getReference() + "元");
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值!");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        // 用户消费进程,模拟消费行为
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                while (true) {
                    //拿到当前的版本号
                    int timestamp = money.getStamp();
                    Integer m = money.getReference();


                    if (m > 10) {
                        System.out.println("大于10元");
                        if (money.compareAndSet(m, m - 10, timestamp,
                                timestamp + 1)) {
                            System.out.println("成功消费10,卡余额:"
                                    + money.getReference());
                            break;
                        }
                    } else {
                        System.out.println("余额不足!");
                        break;
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

运行看输出结果为:

余额小于20,充值成功。余额:39元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:29
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额不足!
余额不足!
余额不足!
余额不足!

我们发现,只为他充值了一次,之后一直消费都是余额不足的状态了。因此当高并发又可能存在ABA的情况下,这样就能彻底杜绝问题了

简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。原理大概就是:维护了一个Pair对象,Pair对象存储我们的对象引用和一个stamp值。每次CAS比较的是两个Pair对象

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

Atomic原子变量类的使用
java.util.concurrent.atomic原子操作包为我们提供了四类原子操作:
提供类如下截图:


原子更新基本类型
AtomicBoolean:布尔型
AtomicInteger:整型
AtomicLong:长整型
原子更新数组
AtomicIntegerArray:数组里的整型
AtomicLongArray:数组里的长整型
AtomicReferenceArray:数组里的引用类型
原子更新引用
AtomicReference<V>:引用类型
AtomicStampedReference:带有版本号的引用类型(可以防止ABA问题)
AtomicMarkableReference:带有标记位的引用类型
原子更新字段
AtomicIntegerFieldUpdater:对象的属性是整型
AtomicLongFieldUpdater:对象的属性是长整型
AtomicReferenceFieldUpdater:对象的属性是引用类型
JDK8新增
DoubleAccumulator、LongAccumulator、
DoubleAdder、LongAdder
是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

原子更新基本类型
这个使用案例就略了,相信大家再使用他们已经0阻碍了

原子更新数组
当你操作的共享是个数组的话,就可以用这个很方便解决问题了

    public static void main(String[] args) {
        AtomicIntegerArray atomicArray = new AtomicIntegerArray(5);
        // 设置指定索引位的数值
        atomicArray.set(0, 5);

        // 也可以通过以下方法设置 (实际上默认值为0,这里加了5)
        // atomicArray.addAndGet(0, 5);

        // -- 0表示角标
        int current = atomicArray.decrementAndGet(0);
        System.out.println("current = " + current);
    }

get(int i):获取数组指定位置的值,并不会改变原来的值
set(int i, int newValue):为数组指定索引位设置一个新值
getAndSet(int i, int newValue):获取数组指定位置的原始值后,用newValue这个新值进行覆盖。
getAndAdd(int i, int delta):获取数组指定索引位的原始值后,为数组指定索引位的值增加delta。那么还有个类似的操作为:addAndGet。
incrementAndGet、decrementAndGet
原子更新引用
使用场景:上面ABA问题有一个非常经典例子,请参加上面

若有类似的使用场景,用对应来存储数据,那么使用这个会非常的方便。例子其实非常简单,这里就不贴出来了,主要介绍一些几个常用的API方法吧:

get()
compareAndSet(V expect, V update):如果当前值与给定的expect相等,(注意是引用相等而不是equals()相等),更新为指定的update值。
.getAndSet(V newValue):原子地设为给定值并返回旧值。
set(V newValue):不管三七二十一,直接把内存里值设置为此值。
原子更新字段
这个可以算是原子更新引用更新引用的一个很好补充。上面根性我们只能全量更新,并且对象的地址都完全变化了。比如我们要更新一个学生的成绩,如果你new一个带有新成绩的Student进来,那就相当于Student对象都变了,显然是不符合我们要求的。

因此java提供了我们针对字段的跟新的原子操作,可谓是一个很好的补充。
当然啦,它使用起来还是稍微有点麻烦的,它是基于反射实现,该字段还不能是private的,且必须被volatile 修饰。

这个在业务上几乎涉及不到,但是在我们框架设计行,还是有可能被适用到的。比如我们内部定义一颗树,可以设计为:

private volatile Node left, right;
1
因为使用极少,因此有兴趣的朋友可以自己去玩玩,这里就略过吧

JDK8新增
DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder

今天看到阿里巴巴的手册里面说 ,如果你使用的JDK8和以上版本,建议使用LongAdder代替AotmicLong

受限于文章篇幅,关于他们的使用以及和LongAdder和AotmicLong的性能测试对比,请移步这篇博文专门讲解:【小家java】AtomicLong可以抛弃了,请使用LongAdder代替(或使用LongAccumulator)

悲观锁和乐观锁(Java都提供了对应支持)
为了更好的理解上面的一些操作原理,本文有必要稍带讲解一些悲观锁和乐观锁的概念以及区别

在本文讲解悲观锁和乐观锁,主要代表是synchronized和CAS的区别

悲观锁
悲观锁是一种独占锁,它假设的前提是“冲突一定会发生”,所以处理某段可能出现数据冲突的代码时,这个代码段就要被某个线程独占。而独占意味着“其它即将执行这段代码的其他线程”都将进入“阻塞”/“挂起”状态。

synchronized关键字就是java对于悲观锁的实现。

由于悲观锁的影响下,其他线程都将进入 阻塞/挂起 状态。而我们知道,CPU执行线程状态切换是要耗费相当资源的,这主要涉及到CPU寄存器的操作。所以悲观锁在性能上不会有太多惊艳的表现(但是一般也不至于成为性能瓶颈,所以各位也不要一棒子打死)

乐观锁
乐观锁假定“冲突不一定会出现”,如果出现冲突则进行重试,直到冲突消失。 由于乐观锁的假定条件,所以乐观锁不会独占资源,性能自然在**多数情况下**就会好于悲观锁。

AtomicInteger是一个标准的乐观锁实现,sun.misc.Unsafe是JDK提供的乐观锁的支持。

    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

为什么是多数情况呢?因为一旦多线程对某个资源的抢占频度达到了某种规模,就会导致乐观锁内部出现多次更新失败的情况,最终造成乐观锁内部进入一种“活锁”状态。这时乐观锁的性能反而没有悲观锁好。

如果我们很好的理解了乐观锁,并且能很熟练的应用的话,我们可以把它运用到我们项目了,帮助改善性能,比一遇到并发问题就去使用悲观锁的选手,显得更加的NB轰轰了有木有
 ———————————————— 


原文链接:https://blog.csdn.net/f641385712/article/details/84933751

这篇关于AtomicLong与LongAdder对比的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927703

相关文章

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议,有时候说的内容比较广需要引用多份的数据,我记录起来有一定难度,所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链接直达:https://www.foxitsoftware.cn/REC/  录屏软件录屏功能就是本职,这款录屏工具在录屏模式上提供了多种选项,可以选择屏幕录制、窗口

类的load方法和initialize方法对比

1. load方法在main()之前被调用,而initialize方法在main()之后调用 load方法实际是在load_images过程中被调用的。load_images会将当前应用依赖的所有镜像(动态库)加载到内存,在在加载中首先是对镜像进行扫描,将所有包含 load 方法的类加入列表 loadable_classes ,然后从这个列表中逐一调用其所包含的 load 方法。 +[XXCl

JavaScript正则表达式六大利器:`test`、`exec`、`match`、`matchAll`、`search`与`replace`详解及对比

在JavaScript中,正则表达式(Regular Expression)是一种用于文本搜索、替换、匹配和验证的强大工具。本文将深入解析与正则表达式相关的几个主要执行方法:test、exec、match、matchAll、search和replace,并对它们进行对比,帮助开发者更好地理解这些方法的使用场景和差异。 正则表达式基础 在深入解析方法之前,先简要回顾一下正则表达式的基础知识。正则

【HarmonyOS】-TaskPool和Worker的对比实践

ArkTS提供了TaskPool与Worker两种多线程并发方案,下面我们将从其工作原理、使用效果对比两种方案的差异,进而选择适用于ArkTS图片编辑场景的并发方案。 TaskPool与Worker工作原理 TaskPool与Worker两种多线程并发能力均是基于 Actor并发模型实现的。Worker主、子线程通过收发消息进行通信;TaskPool基于Worker做了更多场景化的功能封装,例

一些数学经验总结——关于将原一元二次函数增加一些限制条件后最优结果的对比(主要针对公平关切相关的建模)

1.没有分段的情况 原函数为一元二次凹函数(开口向下),如下: 因为要使得其存在正解,必须满足,那么。 上述函数的最优结果为:,。 对应的mathematica代码如下: Clear["Global`*"]f0[x_, a_, b_, c_, d_] := (a*x - b)*(d - c*x);(*(b c+a d)/(2 a c)*)Maximize[{f0[x, a, b,

claude和chatgpt对比:哪一个更适合你?

前言 我们都知道,Claude和ChatGPT都是当前人工智能领域中备受关注的对话生成模型,作为国外AI模型两大巨头,好像他们的实力都不相上下呀! 这时就会有很多同学疑惑,那我如果想选择AI,到底是选择Claude,还是ChatGPT呢?哪个更好呢?他们之间有什么不同独特的地方呢?他们又分别适合在哪些场景使用呢? 技术背景 Claude是由Anthropic公司开发的高性能模型,而Chat

算法复杂度 —— 数据结构前言、算法效率、时间复杂度、空间复杂度、常见复杂度对比、复杂度算法题(旋转数组)

目录 一、数据结构前言 1、数据结构 2、算法 3、学习方法 二、 算法效率 引入概念:算法复杂度  三、时间复杂度 1、大O的渐进表示法 2、时间复杂度计算示例  四、空间复杂度 计算示例:空间复杂度 五、常见复杂度对比 六、复杂度算法题(旋转数组) 1、思路1 2、思路2 3、思路3 一、数据结构前言 1、数据结构         数据结构(D

Matplotlib图像读取和输出及jpg、png格式对比,及透明通道alpha设置

图像像素值 图像像素值一般size为3,也就是通道数,分别代表R,G,B,如果只有单一 一个值则表示灰度值,也就是说一张二维图片,当长和宽都为1080时,那么若是灰度图像,图像尺寸为(1080,1080,1)若是RGB图像则为(1080,1080,3), jpg、png图像格式 jpg图像的灰度值范围和RGB范围为[0,255],数值类型为uint8,也就是无符号整数 png图像的灰度值范

泛型参Class、Class、Class的对比区别

1.原文链接 泛型参Class、Class、Class的对比区别 https://blog.csdn.net/jitianxia68/article/details/73610606 <? extends T>和<? super T> https://www.cnblogs.com/drizzlewithwind/p/6100164.html   2.具体内容: 泛型参数Class、

nano 和 vim对比

nano 和 vim 是两种流行的文本编辑器,各有优缺点和适用场景。以下是对这两种编辑器的详细对比: Nano 优点: 1.简单易用:nano 的界面和命令非常简单,易于新手上手。所有的命令都列在屏幕底部,不需要记住复杂的命令。 2. 直接编辑:打开文件后可以直接开始编辑,不需要进入插入模式。 3. 轻量便捷:通常预装在大多数Linux发行版上,启动速度快。 缺点: 1.功能有限:相比于vim