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

目录
相关文章
|
2月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
77 0
|
5天前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
16 0
|
1月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
1月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。
|
2月前
|
Rust 并行计算 安全
揭秘Rust并发奇技!线程与消息传递背后的秘密,让程序性能飙升的终极奥义!
【8月更文挑战第31天】Rust 以其安全性和高性能著称,其并发模型在现代软件开发中至关重要。通过 `std::thread` 模块,Rust 支持高效的线程管理和数据共享,同时确保内存和线程安全。本文探讨 Rust 的线程与消息传递机制,并通过示例代码展示其应用。例如,使用 `Mutex` 实现线程同步,通过通道(channel)实现线程间安全通信。Rust 的并发模型结合了线程和消息传递的优势,确保了高效且安全的并行执行,适用于高性能和高并发场景。
35 0
|
2月前
|
Java 开发者
【编程高手必备】Java多线程编程实战揭秘:解锁高效并发的秘密武器!
【8月更文挑战第22天】Java多线程编程是提升软件性能的关键技术,可通过继承`Thread`类或实现`Runnable`接口创建线程。为确保数据一致性,可采用`synchronized`关键字或`ReentrantLock`进行线程同步。此外,利用`wait()`和`notify()`方法实现线程间通信。预防死锁策略包括避免嵌套锁定、固定锁顺序及设置获取锁的超时。掌握这些技巧能有效增强程序的并发处理能力。
21 2
|
2月前
|
开发框架 Android开发 iOS开发
跨平台开发的双重奏:Xamarin在不同规模项目中的实战表现与成功故事解析
【8月更文挑战第31天】在移动应用开发领域,选择合适的开发框架至关重要。Xamarin作为一款基于.NET的跨平台解决方案,凭借其独特的代码共享和快速迭代能力,赢得了广泛青睐。本文通过两个案例对比展示Xamarin的优势:一是初创公司利用Xamarin.Forms快速开发出适用于Android和iOS的应用;二是大型企业借助Xamarin实现高性能的原生应用体验及稳定的后端支持。无论是资源有限的小型企业还是需求复杂的大公司,Xamarin均能提供高效灵活的解决方案,彰显其在跨平台开发领域的强大实力。
31 0
|
3月前
|
Java 开发者
Java中的多线程与并发控制
【7月更文挑战第31天】在Java的世界中,多线程是提升程序性能和响应能力的关键。本文将通过实际案例,深入探讨Java多线程的创建、同步机制以及并发包的使用,旨在帮助读者理解并掌握如何在Java中高效地实现多线程编程。
42 3
|
2月前
|
存储 缓存 安全
聊一聊高效并发之线程安全
该文章主要探讨了高效并发中的线程安全问题,包括线程安全的定义、线程安全的类别划分以及实现线程安全的一些方法。