天天看点

【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃

【小家java】java6新特性(简述十大新特性) 鸡肋升级

【小家java】java7新特性(简述八大新特性) 不温不火

【小家java】java8新特性(简述十大新特性) 饱受赞誉

【小家java】java9新特性(简述十大新特性) 褒贬不一

【小家java】java10新特性(简述十大新特性) 小步迭代

【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本

每篇一句

战略上蔑视敌人,但战术上一定要重视敌人

前言

写这篇博文的原因,是因为我今天在看阿里的规范手册的时候(记录在了这里:【小家java】《阿里巴巴 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++需要经过读取-修改-写入三个步骤。举个例子还原一下真相:

  1. 如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10
  2. 线程A对count++,此时count的值为11
  3. 线程B对count++,此时count的值也是11(因为线程B读到的count是10)
  4. 所以到这里应该知道为啥我们的结果是不确定了吧。

怎么破?

要得出正确的结果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有三个操作数:

  1. 内存值V
  2. 旧的预期值A
  3. 要修改的新值B

为了方便大家理解也为了我记忆深刻点,我特意自己尝试着画了一些图解(下同):

【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥

可以发现CAS有两种情况:

  • 如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!
  • 如果内存值V和我们的预期值A不相等,一般也有两种情况:

    1、重试(自旋) 2、什么都不做

CAS失败重试(自旋)

上面的例子,我们启动的100个线程,实质上都对结果进行了+1。但是可以想象到,肯定存在多个线程同一时刻同时想+1的情况,因此可见下图:

【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥

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

CAS失败—什么都不做

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

CAS为什么是原子的呢?

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

解释如下:

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

因为它是一条CPU指令,不会被打断

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

CAS带来的ABA问题

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

  1. 线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
  2. 此时线程A使用CAS将count值修改成100
  3. 修改完后,就在这时,线程B进来了(因为CPU随机,所以是有可能先执行B再执行C的),读取得到count的值为100(内存值和预期值都是100),将count值修改成10
  4. 线程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原子操作包为我们提供了四类原子操作:

提供类如下截图:

【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
  1. 原子更新基本类型

    AtomicBoolean:布尔型

    AtomicInteger:整型

    AtomicLong:长整型

  2. 原子更新数组

    AtomicIntegerArray:数组里的整型

    AtomicLongArray:数组里的长整型

    AtomicReferenceArray:数组里的引用类型

  3. 原子更新引用

    AtomicReference<V>:引用类型

    AtomicStampedReference:带有版本号的引用类型(可以防止ABA问题)

    AtomicMarkableReference:带有标记位的引用类型

  4. 原子更新字段

    AtomicIntegerFieldUpdater:对象的属性是整型

    AtomicLongFieldUpdater:对象的属性是长整型

    AtomicReferenceFieldUpdater:对象的属性是引用类型

  5. 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;
           

因为使用极少,因此有兴趣的朋友可以自己去玩玩,这里就略过吧

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轰轰了有木有

关注A哥

Author A哥(YourBatman)
个人站点 www.yourbatman.cn
E-mail [email protected]
微 信 fsx641385712

活跃平台

【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥
公众号 BAT的乌托邦(ID:BAT-utopia)
知识星球 BAT的乌托邦
每日文章推荐 每日文章推荐
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了关注A哥

继续阅读