• 【面试:并发篇32:cas】原子类型


    【面试:并发篇32:cas】原子类型

    00.前言

    如果有任何问题请指出,感谢。

    01.解释

    JUC提供了一些实现了cas的工具类:三大类 原子整数 原子引用 原子数组

    02.原子整数

    有AtomicBoolean、AtomicInteger、AtomicLong,这里我们拿AtomicInteger举例

            AtomicInteger i = new AtomicInteger(5);
    
            System.out.println(i.incrementAndGet()); // ++i
            System.out.println(i.getAndIncrement()); // i++
    
            System.out.println(i.decrementAndGet()); // --i
            System.out.println(i.getAndDecrement()); // i--
    
            System.out.println(i.getAndAdd(5)); // 先打印再自增5
            System.out.println(i.addAndGet(5)); // 先自增5再打印
    
    	    i.updateAndGet(value -> value * 10);
    	    // 内部是函数式接口 可以使用lambda表达式
    	    // 可以自定义运算方式,更新但没有返回值
    	    i.getAndUpdate(value -> value * 10);
    	    // 返回更新后的返回值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上述都是AtomicInteger类常用的方法 方法对应的操作都是原子的 不会出现线程安全问题 注释中有对应的操作。

    03.原子引用

    补充

    在学下面内容之前,我们要明确一个概念,cas操作中的比较的是地址值或者地址的内容?答案是地址值,下面的ABA问题会体现。

    为什么要有原子引用类型

    我们知道类型分为基本类型(int long char等)和引用类型(类对象),上述我们的原子整数对应的就是基本类型的cas实现,现在的原子引用就是引用类型的cas实现。

    AtomicReference< Object>

    介绍
    泛型里放要使用的引用类型 保证它的原子性
    例子
    还是上一篇文章的取钱例子,只不过把int类型换为了BigDecimal类型,我们现在是否还会出现线程安全问题
    代码

    @Slf4j(topic = "c.Test35")
    public class Test35 {
        public static void main(String[] args) {
            DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
        }
    }
    
    class DecimalAccountCas implements DecimalAccount {
        private AtomicReference<BigDecimal> balance;
    
        public DecimalAccountCas(BigDecimal balance) {
    //        this.balance = balance;
            this.balance = new AtomicReference<>(balance);
        }
    
        @Override
        public BigDecimal getBalance() {
            return balance.get();
        }
    
        @Override
        public void withdraw(BigDecimal amount) {
            while(true) {
                BigDecimal prev = balance.get();
                BigDecimal next = prev.subtract(amount);
                if (balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
    }
    
    interface DecimalAccount {
        // 获取余额
        BigDecimal getBalance();
    
        // 取款
        void withdraw(BigDecimal amount);
    
        /**
         * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
         * 如果初始余额为 10000 那么正确的结果应当是 0
         */
        static void demo(DecimalAccount account) {
            List<Thread> ts = new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                ts.add(new Thread(() -> {
                    account.withdraw(BigDecimal.TEN);
                }));
            }
            ts.forEach(Thread::start);
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println(account.getBalance());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    结果

    0

    解释
    可以看出没有出现线程安全问题

    ABA问题

    介绍
    ABA问题是指 我们现在有三个线程 t1 t2 t3,t1线程想要把字符A更新为C 但是在此之前t2线程把A更新为B,t3线程把B更新为A,问题是:此时t1线程的cas操作是否可以成功 我们上述已经介绍了cas操作比较的是地址 理论上来说cas操作不会成功,但是事实上这个操作是可以成功的

    @Slf4j(topic = "c.TestABA")
    public class TestABA {
    
        static AtomicReference<String> ref = new AtomicReference<>("A");
    
        public static void main(String[] args) throws InterruptedException {
            log.debug("main start...");
            // 获取值 A
            String prev = ref.get();
            other();
            sleep(1);
            // 尝试改为 C
            log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
        }
    
        private static void other() {
            new Thread(() -> {
                log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
            }, "t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果

    15:25:51.824 c.TestABA [main] - main start…
    15:25:51.865 c.TestABA [t1] - change A->B true
    15:25:52.373 c.TestABA [t2] - change B->A true
    15:25:53.381 c.TestABA [main] - change A->C true

    解释
    我们可以看见最后一次cas操作成功了,那有人会想应该这里比较的类型是String,我们又知道String类型的equals方法进行了重写 只要值相同算相同,但还有人认为这里比较不是用equals比较的而是用 == 比较的,比较的是地址 至于为什么相同是因为 我们的"A"是从常量池中获取的 也就是s1 == "A"与s2 == “A” 的地址值都是"A"的地址 所以比较才会相同 进而交换成功。

    探究cas操作比较的是equals还是==

    还是上面那个代码 只不过这次我们直接创建一个String对象 而不是采用从常量池获取的方式,看看这次的ABA是否会成功

    @Slf4j(topic = "c.TestABA")
    public class TestABA {
    
        static AtomicReference<String> ref = new AtomicReference<>(new String("A"));
    
        public static void main(String[] args) throws InterruptedException {
            log.debug("main start...");
            // 获取值 A
            String prev = ref.get();
            other();
            sleep(1);
            // 尝试改为 C
            log.debug("change A->C {}", ref.compareAndSet(prev, new String("C")));
        }
    
        private static void other() {
            new Thread(() -> {
                log.debug("change A->B {}", ref.compareAndSet(ref.get(), new String("B")));
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
            }, "t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果

    15:36:40.437 c.TestABA [main] - main start…
    15:36:40.473 c.TestABA [t1] - change A->B true
    15:36:40.981 c.TestABA [t2] - change B->A true
    15:36:41.985 c.TestABA [main] - change A->C false

    解释
    我们发现这次cas操作竟然失败了,说明了什么?说明cas操作比较的是 == 而不是equals,因为如果是equals 那么s1 == new String(“A”)与s2 == new String(“A”)的比较结果应该相同 cas操作应该成功。综上我们得出了cas操作比较的是 ==

    如何解决ABA问题:AtomicStampedReference

    介绍
    AtomicStampedReference是AtomicReference的升级,它加入了计时器(版本号) 每进行一次操作计时器就会改变,最后进行cas对比时 比较的不仅仅是地址还有计时器是否相同
    对上述代码优化

    @Slf4j(topic = "c.Test36")
    public class Test36 {
    
        static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    
        public static void main(String[] args) throws InterruptedException {
            log.debug("main start...");
            // 获取值 A
            String prev = ref.getReference();
            // 获取版本号
            int stamp = ref.getStamp();
            log.debug("版本 {}", stamp);
            // 如果中间有其它线程干扰,发生了 ABA 现象
            other();
            sleep(1);
            // 尝试改为 C
            log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
        }
    
        private static void other() {
            new Thread(() -> {
                log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
                log.debug("更新版本为 {}", ref.getStamp());
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
                log.debug("更新版本为 {}", ref.getStamp());
            }, "t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    结果

    15:42:58.652 c.Test36 [main] - main start…
    15:42:58.655 c.Test36 [main] - 版本 0
    15:42:58.709 c.Test36 [t1] - change A->B true
    15:42:58.709 c.Test36 [t1] - 更新版本为 1
    15:42:59.213 c.Test36 [t2] - change B->A true
    15:42:59.214 c.Test36 [t2] - 更新版本为 2
    15:43:00.227 c.Test36 [main] - change A->C false

    解释
    我们发现这次交换失败,原因就是上面所讲增加了计时器,之后的每一次cas操作不仅需要传 最新值和要更新的值 还需要传 版本号和要更新的版本号,最终解决了ABA问题。

    注意

    因为基本类型的地址值就是它们的值本身 所以原子整数这一类 也会发生ABA问题

    AtomicMarkableReference

    AtomicMarkableReference方法和AtomicStampedReference基本一致,不同的是AtomicMarkableReference方法把计时器改为了boolean类型 也就是标记,如果改过就改变标记 最后比较时 比较一下标记是否一样,不过这里有一个问题 就是 因为boolean只有两种状态true与false 也就是它只能保证解决AB问题或者降低ABA问题出现的几率,但是不能解决ABA问题。
    AtomicMarkableReference方法因为和AtomicStampedReference方法用法基本一致这里不再举例。

    04.原子数组

    AtomicIntegerArray:原子整型数组
    AtomicLongArray:原子长整型数组
    AtomicReferenceArray:原子引用数组
    这里我们拿AtomicIntegerArray举例

    介绍

    为什么需要原子数组,因为我们之前的数组在新增元素的时候是线程不安全的。

    原子数组使用

    例子介绍
    创建十个线程 与一个 公共的int数组长度为10,每个线程分别往对应的数组下标元素里面加10000次1,例如线程0就向a[0]加1,最后打印数组,理论上的结果应该是数组的十个元素都是10000。
    代码
    下面代码用到了lambda函数式接口的知识,如果不会的同学可以看我之前的lambda表达式的文章
    https://blog.csdn.net/m0_71229547/article/details/125046910?spm=1001.2014.3001.5501
    https://blog.csdn.net/m0_71229547/article/details/125093487?spm=1001.2014.3001.5501

    public class Test39 {
    
        public static void main(String[] args) {
            demo(
                    ()->new int[10],
                    (array)->array.length,
                    (array, index) -> array[index]++,
                    array-> System.out.println(Arrays.toString(array))
            );
    
            demo(
                    ()-> new AtomicIntegerArray(10),
                    (array) -> array.length(),
                    (array, index) -> array.getAndIncrement(index),
                    array -> System.out.println(array)
            );
        }
    
        /**
         参数1,提供数组、可以是线程不安全数组或线程安全数组
         参数2,获取数组长度的方法
         参数3,自增方法,回传 array, index
         参数4,打印数组的方法
         */
        // supplier 提供者 无中生有  ()->结果
        // function 函数   一个参数一个结果   (参数)->结果  ,  BiFunction (参数1,参数2)->结果
        // consumer 消费者 一个参数没结果  (参数)->void,      BiConsumer (参数1,参数2)->
        private static <T> void demo(
                Supplier<T> arraySupplier,
                Function<T, Integer> lengthFun,
                BiConsumer<T, Integer> putConsumer,
                Consumer<T> printConsumer ) {
            List<Thread> ts = new ArrayList<>();
            T array = arraySupplier.get();
            int length = lengthFun.apply(array);
            for (int i = 0; i < length; i++) {
                // 每个线程对数组作 10000 次操作
                ts.add(new Thread(() -> {
                    for (int j = 0; j < 10000; j++) {
                        putConsumer.accept(array, j%length);
                    }
                }));
            }
    
            ts.forEach(t -> t.start()); // 启动所有线程
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });     // 等所有线程结束
            printConsumer.accept(array);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    结果

    [9335, 9298, 9263, 9254, 9318, 9260, 9248, 9229, 9230, 9242]
    [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

    解释
    可以看出普通数组存在并发安全问题,但是原子数组并没有这样的问题。

    原子数组的ABA问题

    ABA出现的原因
    我们先来分析为什么会有ABA问题,究其根本就是cas比较时地址没有改变 但其实以及改变过了。
    原子数组的cas实现

    我们注意到compareAndSet的3个参数,i代表的是数组偏移值 expect代码的是数组偏移值对应元素的旧值 update代表的是偏移值对应元素的要更新的值。
    我们注意几个点

    1.原子数组cas比较的是数组里面偏移值对应的元素,而不是数组与数组之间的比较
    2.既然是数组元素的比较,所以因为数组类型的不同就有可能出现ABA问题

    例子

    @Slf4j(topic = "c.TestABA")
    public class TestABA {
    
        static AtomicIntegerArray ref = new AtomicIntegerArray(10);
    
        public static void main(String[] args) throws InterruptedException {
            log.debug("main start...");
            // 获取值 A
            int prev = ref.get(1);
            other();
            sleep(1);
            // 尝试改为 C
            log.debug("change A->C {}", ref.compareAndSet(1,prev,2));
        }
    
        private static void other() {
            new Thread(() -> {
                log.debug("change A->B {}", ref.compareAndSet(1,ref.get(1),1));
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                log.debug("change B->A {}", ref.compareAndSet(1,ref.get(1), 0));
            }, "t2").start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    结果

    16:41:43.219 c.TestABA [main] - main start…
    16:41:43.274 c.TestABA [t1] - change 0->1 true
    16:41:43.790 c.TestABA [t2] - change 1->0 true
    16:41:44.790 c.TestABA [main] - change 0->2 true

    解释
    可以看出用原子整数数组类型 发生了ABA问题,究其根本就是因为AtomicIntegerArray内部实现是int为基本类型所以会出现cas比较成功的问题,那我们用AtomicReferenceArray 原子引用数组 会有这个问题吗?一样会有

    如何解决原子数组的ABA问题
    AtomicReferenceArray<>的泛型中填AtomicStampedReference类型,这样保证在数组元素比较时 有版本号保证 不会出现ABA问题。

  • 相关阅读:
    小芯片chiplet技术杂谈
    6.Flask-APScheduler定时任务框架
    突破耐盐水稻生物育种 国稻种芯-何登骥:粮安向盐碱地要粮
    MySQL基本操作之数据库设计理论
    iOS原生与H5交互方法
    数据库理论知识及相关发展方向
    苍穹外卖-01
    Docker实战-部署GPE微服务的监控体系(二)
    docker网络模式--资源分配叙述(1)
    农村当前最大的红利,以及三大赚钱项目
  • 原文地址:https://blog.csdn.net/m0_71229547/article/details/126062151