【多线程:cas】无锁实现并发

简介: 【多线程:cas】无锁实现并发

【多线程:cas】无锁实现并发

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");
    }
}
AI 代码解读

结果

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;
            }
        }
    }
AI 代码解读

此方法内写了一个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可以与最新值比较
AI 代码解读

总结

总的来说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循环,所以在竞争激烈的情况下 重试次数增多影响效率

相关文章
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
574 0
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
153 83
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
162 83
并发设计模式实战系列(4):线程池
需要建立持续的性能剖析(Profiling)和调优机制。通过以上十二个维度的系统化扩展,构建了一个从。设置合理队列容量/拒绝策略。动态扩容/优化任务处理速度。检查线程栈定位热点代码。调整最大用户进程数限制。CPU占用率100%
183 0
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
121 0
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
453 59
|
8月前
|
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
128 6
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
182 8
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问