• java多线程之CAS无锁


    1.背景

    加锁确实能解决线程并发的的问题,但是会造成线程阻塞等待等问题

    那么有没有一种方法,既可以线程安全,又不会造成线程阻塞呢?

    答案是肯定的......请看如下案例

    注意:重要的文字说明,写在了代码注释上,这样便于大家理解,请阅读代码和注释加以理解;

    2.取钱案例引出问题

    启动10000个线程,每个线程减去10元

    原来账户共10000 0元

    正常情况账户最后的余额应该是0元

    测试多线程并发问题

    先定义一个通用的接口,后面使用不同实现来测试

    账户Money接口:

    package com.ldp.demo05;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:14
     * @description
     */
    public interface Money {
        // 获取余额
        Integer getBalance();
    
        // 取款
        void reduce(Integer amount);
    
        /**
         * 启动10000个线程,每个线程减去10元
         * 原来账户共10000 0元
         * 正常情况应该是0元
         * 测试多线程并发问题
         *
         * @param account
         */
        static void handel(Money account) {
            List<Thread> ts = new ArrayList<>();
            long start = System.nanoTime();
            for (int i = 0; i < 10000; i++) {
                ts.add(new Thread(() -> {
                    account.reduce(10);
                }));
            }
            ts.forEach(Thread::start);
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.nanoTime();
            System.out.println("当前余额:" + account.getBalance()
                    + " ,耗时: " + (end - start) / 1000_000 + " ms");
        }
    }
    View Code

    2.1.存在线程安全的解决方案

    package com.ldp.demo05.impl;
    
    import com.ldp.demo05.Money;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:17
     * @description
     */
    public class UnSafeMoney implements Money {
        private Integer balance;
    
        public UnSafeMoney(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public Integer getBalance() {
            return balance;
        }
    
        @Override
        public void reduce(Integer amount) {
            // 存在线程不安全
            balance -= amount;
        }
    }
    View Code

    2.2.使用传统锁的解决方案

    package com.ldp.demo05.impl;
    
    import com.ldp.demo05.Money;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:17
     * @description
     */
    public class SyncSafeMoney implements Money {
        private Integer balance;
    
        public SyncSafeMoney(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public Integer getBalance() {
            return balance;
        }
    
        @Override
        public synchronized void reduce(Integer amount) {
            // 当前对象加锁 安全
            balance -= amount;
        }
    }
    View Code

    2.3.使用CAS无锁的解决方案

    package com.ldp.demo05.impl;
    
    import com.ldp.demo05.Money;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:30
     * @description <p>
     * 无锁的思路
     *
     * </p>
     */
    public class CASSafeMoney implements Money {
        private AtomicInteger balance;
    
        public CASSafeMoney(Integer balance) {
            this.balance = new AtomicInteger(balance);
        }
    
        @Override
        public Integer getBalance() {
            return balance.get();
        }
    
        /**
         * compareAndSet 做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败
         * <p>
         * 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
         * 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再
         * 开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
         * <p>
         * CAS 的特点
         * 结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
         * 1.CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试在执行【修改】。
         * 2.synchronized 是基于悲观锁的思想:最悲观的估计,防着其它线程来修改共享变量,当前线程上锁后,其他线程阻塞等待。
         * 3.CAS 体现的是【无锁并发、无阻塞并发】。
         * 因为没有使用 synchronized,所以线程【不会陷入阻塞】,这是效率提升的因素之一,
         * 但如果竞争激烈,可以想到重试必然频繁发生,反而频繁切换上下文,效率会受影响。
         * 4.特别注意:
         * 无锁情况下,但如果竞争激烈,因为线程要保持运行,需要CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
         *
         * @param amount
         */
        @Override
        public void reduce(Integer amount) {
            // 不断尝试直到成功为止
            while (true) {
                // 修改前的值
                int prev = balance.get();
                // 修改后的值
                int next = prev - amount;
                // 执行修改 compareAndSet  使用CAS乐观锁实现
                if (balance.compareAndSet(prev, next)) {
                    break;
                }
            }
            // 简要写法
            // balance.addAndGet(-1 * amount);
        }
    }
    View Code

    3.测试

    package com.ldp.demo05;
    
    import com.ldp.demo05.impl.CASSafeMoney;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:20
     * @description
     */
    public class Test01Money {
        public static void main(String[] args) {
            // 1.无锁不安全 当前余额:29530 ,耗时: 4847 ms
            // Money.handel(new UnSafeMoney(100000));
    
            // 2.synchronized加锁安全 当前余额:0 ,耗时: 7386 ms
            // Money.handel(new SyncSafeMoney(100000));
    
            // 3.使用乐观锁 CAS  当前余额:0 ,耗时: 3466 ms
            Money.handel(new CASSafeMoney(100000));
        }
    }
    View Code

    4.CompareAndSet 方法分析

    package com.ldp.demo05;
    
    import com.common.MyThreadUtil;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @author 姿势帝-博客园
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 02/16 8:49
     * @description
     */
    @Slf4j
    public class Test02CompareAndSet {
        /**
         * 观察多线程修改值
         *
         * @param args
         */
        public static void main(String[] args) {
            AtomicInteger n = new AtomicInteger(100);
            int mainPrev = n.get();
            log.info("当前值:{}", n.get());
            // 线程 t1 将其修改为 90
            new Thread(() -> {
                // 模拟睡眠3秒
                MyThreadUtil.sleep(1);
                boolean b = n.compareAndSet(mainPrev, 90);
                log.info("修改结果:{}", b);
            }, "t1").start();
    
            // 模拟睡眠3秒
            MyThreadUtil.sleep(2);
            new Thread(() -> {
                boolean b = n.compareAndSet(mainPrev, 80);
                log.info("修改结果:{}", b);
            }, "t2").start();
            // 最后结果值
            MyThreadUtil.sleep(2);
            log.info("最后值为={}", n.get());
            // 21:04:15.369 [main] -> 当前值:100
            // 21:04:16.451 [t1] -> 修改结果:true
            // 21:04:17.457 [t2] -> 修改结果:false
            // 21:04:19.457 [main] -> 最后值为=90
        }
    }
    View Code

    完美!

  • 相关阅读:
    经典矩阵dp寻找递增最大长度
    有符号char转无符号short
    正则表达式学习之grep,sed和awk
    Git学习总结
    Linux下的压缩及归档
    bash脚本编程学习笔记(二)
    bash脚本编程学习笔记(一)
    Linux磁盘及文件系统(三)Linux文件系统
    Linux磁盘及文件系统(二)Linux下磁盘命名和分区
    Linux磁盘及文件系统(一)
  • 原文地址:https://www.cnblogs.com/newAndHui/p/15912116.html
Copyright © 2020-2023  润新知