【多线程: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");
    }
}

结果

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;
            }
        }
    }

此方法内写了一个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循环,所以在竞争激烈的情况下 重试次数增多影响效率

目录
相关文章
|
1月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
152 0
|
22天前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
127 59
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
90 0
|
12天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
1月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
26 1
|
1月前
|
安全
【多线程】CAS、ABA问题详解
【多线程】CAS、ABA问题详解
20 0
|
3月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
57 0
|
2月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。