• 【面试:并发篇31:多线程:cas】无锁实现并发


    【面试:并发篇31:多线程:cas】无锁实现并发

    00.前言

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

    01.介绍

    cas

    cas可以实现无锁并发,无阻塞并发。

    cas与synchronized对比

    CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试。
    synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

    02.例子

    例子介绍

    我们创建一个账户 可以取账户里的钱 现在账户里有10000,每次取10块,我们创建1000个线程 每个线程都执行一次取钱操作,如果没有线程安全的情况下 最后账户的余额应该是0,但是我们知道一定会有线程安全问题,所以我们用synchronized与cas分别实现它,最后来分析cas为什么要这样处理。

    代码

    public class TestAccount {
        public static void main(String[] args) {
            Account account = new AccountCas(10000);
            Account.demo(account); // cas实现
    
            AccountUnsafe account1 = new AccountUnsafe(10000);
            Account.demo(account1); // synchronized实现
        }
    }
    
    class AccountCas implements Account {
        private AtomicInteger balance;
    
        public AccountCas(int balance) {
            this.balance = new AtomicInteger(balance);
        }
    
        @Override
        public Integer getBalance() {
            return balance.get();
        }
    
        @Override
        public void withdraw(Integer amount) {
            while(true) {
                // 获取余额的最新值
                int prev = balance.get();
                // 要修改的余额
                int next = prev - amount;
                // 真正修改
                if(balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
    }
    
    class AccountUnsafe implements Account {
    
        private Integer balance;
    
        public AccountUnsafe(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public Integer getBalance() {
            synchronized (this) {
                return this.balance;
            }
        }
    
        @Override
        public void withdraw(Integer amount) {
            synchronized (this) {
                this.balance -= amount;
            }
        }
    }
    
    interface Account {
        // 获取余额
        Integer getBalance();
    
        // 取款
        void withdraw(Integer amount);
    
        /**
         * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
         * 如果初始余额为 10000 那么正确的结果应当是 0
         */
        static void demo(Account account) {
            List<Thread> ts = new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                ts.add(new Thread(() -> {
                    account.withdraw(10);
                }));
            }
            long start = System.nanoTime();
            ts.forEach(Thread::start);
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.nanoTime();
            System.out.println(account.getBalance()
                    + " cost: " + (end-start)/1000_000 + " ms");
        }
    }
    
    • 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
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    结果

    0 cost: 84 ms
    0 cost: 88 ms

    解释
    可以看出两种方式都保证了线程安全。
    我们主要来分析一下cas代码的操作,我们创建了一个Account接口 里面写了两个抽象方法分别用来取款和查看余额 写了一个静态方法demo用来创建1000个线程并且取款10元 最终我们查看余额和用时,接下来我们创建了AccountCas类 实现Account接口,我们注意取钱方法withdraw的实现

    public void withdraw(Integer amount) {
            while(true) {
                // 获取余额的最新值
                int prev = balance.get();
                // 要修改的余额
                int next = prev - amount;
                // 真正修改
                if(balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    此方法内写了一个while循环 退出条件是 balance.compareAndSet(prev, next),发现调用了balance调用了compareAndSet方法,balance是原子整数类型 compareAndSet方法就是我们所说的cas(也可以认为是compareAndSwap),它的有两个参数prev next,分别代表 最新值、修改后的值。compareAndSet方法的作用是:判断当前的balance.get()返回的是不是最新值 如果不是则返回false,如果是则返回true且更新值并且同步到主存,那么什么情况下会返回false?当其他线程更改了最新的数据 但是当前线程还没有获取到最新值时 当前线程的最新值会和主存现在的最新值进行对比 如果不一样则说明有其他线程已经对值进行了修改 此时返回false,然后继续循环 直到更新成功。

    可见性分析

    我们查看AtomicInteger类的源码,看看它是如何实现的

    我们注意到value被volatile修饰 我们具体的数值也是保存在value中的,所以保证了可见性 即每次更新 balance时也把它同步到主存中 每次读取时也能获取最新值 ,也保证了compareAndSet可以与最新值比较

    总结

    总的来说cas就是保证了可见性的条件下 进行自旋。

    03.cas具体流程分析

    我们把代码里的线程改为1个,然后调用debug模式 然后手动把balance的value值进行更改,也就是我们自己充当另一个线程 更改debug的线程,我们来看其他线程更改value后 代码的执行情况

    我们把value改为了9000 然后compareAndSet方法进行比较后发现不是最新值 然后返回了false 再次进入循环 此时balance.get()获取到了最新值9000,再次进入if执行compareAndSet方法发现这次是最新值 说明可以更新 然后更新value为8900 并且返回true 退出循环

    04.cas效率分析

    上下文切换对于效率的影响

    当cpu核心数比较多时,cas效率要高于synchronized,因为影响效率的主要是上下文切换 也就是运行状态的改变,比如我们用synchronized时获取锁的过程就是 从运行状态去争抢锁 如果争抢失败改变状态为BLOCKED状态,但是对于cas来说只要有一个cpu一直执行while循环 就能保证cas一直处于运行状态 直到成功进入while循环,不过这是cpu核心数多情况下 在cpu核心数不够时很有可能 还是会发生上下文切换 从运行态到可运行态的过程。

    竞争激烈对于效率的影响

    如果线程直接竞争过于激烈,势必会导致cas需要多次判断重试 直到返回true进入while循环,所以在竞争激烈的情况下 重试次数增多影响效率

  • 相关阅读:
    JavaWeb搭建学生管理系统(手把手)
    .net core 到底行不行!超高稳定性和性能的客服系统:性能实测
    MySQL数据库四:MySQL数据库
    一本书读懂大数据 读书笔记(1)
    Ansys Zemax|在设计抬头显示器(HUD)时需要使用哪些工具?
    Java网络编程 - 网络基础、Socket网络编程、TCP和UDP网路编程、URL编程
    实战监听 Eureka client 的缓存更新
    保护环GuardRing(基于IC617)
    为啥一到秋季就鼻塞、流鼻涕、打喷嚏?该如何是好?别总当成感冒
    Java开发学习(六)----DI依赖注入之setter及构造器注入解析
  • 原文地址:https://blog.csdn.net/m0_71229547/article/details/126062086