【多线程: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 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
234 0
|
1月前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
198 59
|
1月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
41 6
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
58 6
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
4月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
95 0
|
1月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
2月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
36 1
|
2月前
|
安全
【多线程】CAS、ABA问题详解
【多线程】CAS、ABA问题详解
33 0
|
4月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题