• CAS都不了解,你还怎么看J.U.C


    前言

    说到CAS(CompareAndSwap),不得不先说一说悲观锁乐观锁,因为CAS是乐观锁思想的一种实现。

    悲观锁:总是很悲观的认为,每次拿数据都会有其他线程并发执行,所以每次都会进行加锁,用完之后释放锁,其他的线程才能拿到锁,进而拿到资源进行操作。java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

    乐观锁:总是很乐观认为,自己拿到数据操作的时候,没有其他线程来并发操作,等自己操作结束要更新数据时,判断自己对数据操作的期间有没有其他线程进行操作,如果有,则进行重试,直到操作变更成功。乐观锁常使用CAS和版本号机制来实现。java中java.util.atomic包下的原子类都是基于CAS实现的。

    一、什么是CAS

    CAS指CompareAndSwap,顾名思义,先比较后交换。比较什么?交换什么呢?

    CAS中有三个变量:内存地址V,期待值A, 更新值B。

    当且仅当内存地址V对应的值与期待值A时相等时,将内存地址V对应的值更换为B。

    二、atomic包

    有了悲观锁,乐观锁的知识,让我们走进java.util.atomic包,看一看java中CAS的实现。

    这就是java.util.atomic包下的类,我们着重看AtomicInteger源码(其他的都是一样的思想实现的)

    然后思考CAS有什么弊端?如何解决弊端?有什么优缺点?

    2.1、走进AtomicInteger源码

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;

        // 使用Unsafe.compareAndSwapInt进行原子更新操作
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        //value对应的存储地址偏移量
        private static final long valueOffset;

        static {
            try {
                //使用反射及unsafe.objectFieldOffset拿到value字段的内存地址偏移量,这个值是固定不变的
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }

        //volatile修饰的共享变量
        private volatile int value;
        //..........
        }

    上面的代码其实就是为了初始化内存值对应的内存地址偏移量valueOffset,方便后续执行CAS操作时使用。因为这个值一旦初始化,就不会更改,所以使用static final 修饰。

    我们可以看到value使用了volatile修饰,上一篇9龙详细详解了JMM,其中也说了volatile的语义,不了解的小伙伴可以先去看一看。

    我们都知道如果进行value++操作,并发下是不安全的。上一篇中我们也通过例子证明了volatile只能保证可见性,不能保证原子性。因为value++本身不是原子操作,value++分了三步,先拿到value的值,进行+1,再赋值回value。

    2.2、compareAndSwapXxx

    我们先看一看AtomicInteger提供的CAS操作。

        /**
         * 原子地将value设置为update,如果valueOffset对应的值与expect相等时
         *
         * @param expect 期待值
         * @param update 更新值
         * @return 如果更新成功,返回true;在valueOffset对应的值与expect不相等时返回false
         */

        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }

    我们已经知道CAS的原理,那来看看下面的测试。你知道输出的结果是多少吗?评论区给出你的答案吧。

    public class AtomicIntegerTest {
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger();
            atomicInteger.compareAndSet(01);
            atomicInteger.compareAndSet(21);
            atomicInteger.compareAndSet(13);
            atomicInteger.compareAndSet(24);
            System.out.println(atomicInteger.get());
        }
    }

    Unsafe提供了三个原子更新的方法。

    关于Unsafe类,因为java不支持直接操作底层硬件资源,如分配内存等。如果你使用unsafe开辟的内存,是不被JVM垃圾回收管理,需要自己管理,容易造成内存泄漏等。

    2.3、AtomicInteger的原子自增方法

    我们上面说了,value++不是原子操作,不能在并发下使用。我们来看看AtomicInteger提供的原子++操作。

        /**
         * 原子地对value进行+1操作
         *
         * @return 返回更新后的值
         */

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

        /**
         * unsafe提供的方法
         * var1 更改的目标对象
         * var2 目标对象的共享字段对应的内存地址偏移量valueOffset
         * var4 需要在原value上增加的值
         * @return 返回未更新前的值
         */

         public final int getAndAddInt(Object var1, long var2, int var4) {
            //期待值
            int var5;
            do {
                //获取valueOffset对应的value的值,支持volatile load
                var5 = this.getIntVolatile(var1, var2);
                //如果原子更新失败,则一直重试,直到成功。
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

            return var5;
        }

    我们看到CAS只能原子的更新一个值,如果我们要原子更新多个值,CAS可以做到吗?答案是可以的。

    2.4、AtomicReference

    如果要原子地更新多个值,就需要使用AtomicReference。其使用的是compareAndSwapObject方法。可以将多个值封装到一个对象中,原子地更换对象来实现原子更新多个值。

    public class MultiValue {
        private int value1;
        private long value2;
        private Integer value3;

        public MultiValue(int value1, long value2, Integer value3) {
            this.value1 = value1;
            this.value2 = value2;
            this.value3 = value3;
        }
     }

    public class AtomicReferenceTest {
        public static void main(String[] args) {
            MultiValue multiValue1 = new MultiValue(111);
            MultiValue multiValue2 = new MultiValue(222);
            MultiValue multiValue3 = new MultiValue(333);
            AtomicReference<MultiValue> atomicReference = new AtomicReference<>();
            //因为构造AtomicReference时,没有使用有参构造函数,所以value默认值是null
            atomicReference.compareAndSet(null, multiValue1);
            System.out.println(atomicReference.get());
            atomicReference.compareAndSet(multiValue1, multiValue2);
            System.out.println(atomicReference.get());
            atomicReference.compareAndSet(multiValue2, multiValue3);
            System.out.println(atomicReference.get());
        }
    }
    //输出结果
    //MultiValue{value1=1, value2=1, value3=1}
    //MultiValue{value1=2, value2=2, value3=2}
    //MultiValue{value1=3, value2=3, value3=3}

    我们再看一看AtomicReference的compareAndSet方法。

    注意:这里的比较都是使用==而非equals方法。所以最好封装的MultiValue不要提供set方法。

     public final boolean compareAndSet(V expect, V update) {
         return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
     }

    2.5、CAS的ABA问题

    假设你的账户上有100块钱,你要给女票转50块钱。

    我们使用CAS进行原子更新账户余额。由于某种原因,你第一次点击转账出现错误,你以为没有发起转账请求,这时候你又点击了一次。系统开启了两个线程进行转账操作,第一个线程进行CAS比较,发现你的账户上预期是100块钱,实际也有100块钱,这时候转走了50,需要设置为100 - 50 = 50 元,这时账户余额为50

    第一个线程操作成功了,第二个线程由于某种原因阻塞住了;这时候,你的家人又给你转了50块钱,并且转账成功。那你账户上现在又是100块钱;

    太巧了,第二个线程被唤醒了,发现你的账户是100块钱,跟预期的100是相等的,这时候又CAS为50。大兄弟,哭惨了,你算算,正确的场景你要有多少钱?这就是CAS存在的ABA问题。

    public class AtomicIntegerABA {

        private static AtomicInteger atomicInteger = new AtomicInteger(100);

        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);

            //线程1
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
                atomicInteger.compareAndSet(10050);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            });

            //线程2
            executorService.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
                atomicInteger.compareAndSet(50100);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            });

            //线程3
            executorService.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
                atomicInteger.compareAndSet(10050);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            });

            executorService.shutdown();
        }
    }
    //输出结果
    //pool-1-thread-1 - 100
    //pool-1-thread-1 - 50
    //pool-1-thread-2 - 50
    //pool-1-thread-2 - 100
    //pool-1-thread-3 - 100
    //pool-1-thread-3 - 50

    大家心想,靠,这不是坑吗?那还用。。。。。。。。。。。。。。冷静,冷静。你能想到的问题,jdk都能想到。atomic包提供了一个AtomicStampedReference

    2.6、AtomicStampedReference

    看名字是不是跟AtomicReference很像啊,其实就是在AtomicReference上加上了一个版本号,每次操作都对版本号进行自增,那每次CAS不仅要比较value,还要比较stamp,当且仅当两者都相等,才能够进行更新。

    public AtomicStampedReference(V initialRef, int initialStamp) {
            pair = Pair.of(initialRef, initialStamp);
        }
    //定义了内部静态内部类Pair,将构造函数初始化的值与版本号构造一个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);
            }
        }

    //所以我们之前的value就对应为现在的pair
        private volatile Pair<V> pair;

    让我们来看一看它的CAS方法。

     public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp)
     
    {
            Pair<V> current = pair;
            return
                //只有在旧值与旧版本号都相同的时候才会更新为新值,新版本号
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }

    还是上面转账的例子,我们使用AtomicStampedReference来看看是否解决了呢。

    public class AtomicStampedReferenceABA {
        /**
         * 初始化账户中有100块钱,版本号对应0
         */

        private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1000);

        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            int[] result = new int[1];
            //线程1
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
                //将100更新为50,版本号+1
                atomicInteger.compareAndSet(1005001);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            });

            //线程2
            executorService.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
                //将50更新为100,版本号+1
                atomicInteger.compareAndSet(5010012);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            });

            //线程3
            executorService.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
                //此线程还是以为没有其他线程进行过更改,所以旧版本号还是0
                atomicInteger.compareAndSet(1005001);
                System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            });

            executorService.shutdown();
        }
    }
    //输出结果
    //pool-1-thread-1 - 100
    //pool-1-thread-1 - 50
    //pool-1-thread-2 - 50
    //pool-1-thread-2 - 100
    //pool-1-thread-3 - 100
    //pool-1-thread-3 - 100

    妈妈再也不用担心我的钱少了。

    三、总结

    本篇详细讲解了CAS的原理,CAS可以进行原子更新一个值(包括对象),主要用于读多写少的场景,如原子自增操作,如果多线程调用,在CAS失败之后,会死循环一直重试,直到更新成功。这种情况是很耗CPU资源的,虽然没有锁,但循环的自旋可能比锁的代价还高。同时存在ABA问题,但AtomicStampedReference通过加入版本号机制已经解决。其实对于atomic包,jdk1.8新增的LongAdder,效率比AtomicLong高,9龙还未涉足,以后肯定会品一品。J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,如果不了解CAS,怎么入手J.U.C呢。

    各位看官,如果觉得9龙的文章对你有帮助,求点赞,求关注。如果转载请注明出处。

    参考链接:

    java中Unsafe使用讲解

  • 相关阅读:
    hive报错 java.sql.SQLException: No suitable driver found for jdbc:hive://localhost:10000/default
    使用Beeline连接Hive
    hive报错 root is not allowed to impersonate root (state=08S01,code=0)
    hive报错 Could not open client transport with JDBC Uri: jdbc:hive2://node01:10000/default:java.net.ConnectException refused
    excel快速删除空值单元格,数据上移
    FineBI 图表选择
    数据库连接池大小设置?
    工作中有待留❤️积累的一些经验
    内存包括随机存储器(RAM),只读存储器(ROM),以及高速缓存(CACHE)。RAM最重要
    我自己ood的复习思路:抄
  • 原文地址:https://www.cnblogs.com/9dragon/p/12023971.html
Copyright © 2020-2023  润新知