【多线程】CAS

这篇具有很好参考价值的文章主要介绍了【多线程】CAS。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【多线程】CAS

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇

🐍一. 什么是 CAS

CAS 是操作系统/硬件,给 JVM 提供的另外一种更轻量的原子操作的机制

CAS: 全称Compare and swap,字面意思: " 比较并交换 ",一个 CAS 涉及到以下操作:

比较内存和寄存器的值,如果相等,则把寄存器和另一个内存中的值进行交换,如果不相等不进行操作

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)

  2. 如果比较相等,将 B 写入 V。(交换)

  3. 返回操作是否成功。

CAS 伪代码

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

address 内存地址

expectValue 用来比较的值(寄存器)

swapValue 用来交换的值(另一个寄存器)

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式)


🦎二. CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;

  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;

  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

简而言之,是因为硬件予以了支持,软件层面才能做到


🦖三. CAS 典型应用场景

🐶1. 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.

典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

例如我们之前 count 在两个线程中都自增 5000,结果却不是 10000,在这里就可以解决这个问题

public class Demo27 {
    //public static int count = 0;
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for (int i = 0; i < 50000; i++){
               //count++;
               //这个方法就相当于 count++;
               count.getAndIncrement();
           }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //count++;
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

【多线程】CAS

伪代码实现:

class AtomicInteger {
    private int value;
    
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

内部实现过程:两个线程同时调用 getAndIncrement

  1. 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)

线程1的 oldvalue = 0,线程2的 oldvalue = 0,Value = 0 。

  1. 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.

注意:

  • CAS 是直接读写内存的, 而不是操作寄存器.

  • CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的.

线程1的 oldvalue = 0,线程2的 oldvalue = 0,Value = 1 。

  1. 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要

进入循环.

在循环里重新读取 value 的值赋给 oldValue

线程1的 oldvalue = 0,线程2的 oldvalue = 1,Value = 1 。

  1. 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.

线程1的 oldvalue = 0,线程2的 oldvalue = 1,Value = 2 。

  1. 线程1 和 线程2 返回各自的 oldValue 的值即可.

通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.


🐱2. 实现自旋锁

基于 CAS 实现更灵活的锁, 获取到更多的控制权.

自旋锁伪代码

public class SpinLock {
    private Thread owner = null;
    
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    
    public void unlock (){
        this.owner = null;
   }
}

当 owner 为 null 的时候 CAS 才能成功,循环才能结束;当 owner 为非 null 的时候,说明当前的锁已经被其他的线程占用了,因此就需要继续循环。


🦕四. CAS 的 ABA 问题

🐭1. 什么是 ABA 问题

在 CAS 中无法区分,数据始终是 A ,还是从 A -> B -> A 。


🐹2. ABA 问题引来的 BUG

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50

操作.

我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.

如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
 
正常的过程:

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.

  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

 
异常的过程:

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.

  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!

  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

 
这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼!!


🐰3. 解决方案

正确的解决 ABA 问题的办法,是想办法获取到中间过程,于是引入了一个 “版本号” 来解决

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.

  • 真正修改的时候,

如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.

如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

这就好比, 判定这个手机是否是翻新机, 那么就需要收集每个手机的数据, 第一次挂在电商网站上的

手机记为版本1, 以后每次这个手机出现在电商网站上, 就把版本号进行递增. 这样如果买家不在意

这是翻新机, 就买. 如果买家在意, 就可以直接略过.

对比理解上面的转账例子

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50

操作.

我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.

为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.

  1. 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 版本号为 1, 期望更新为 50.

  2. 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.

  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.

  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读

 
到的版本号为 1, 版本小于当前版本, 认为操作失败.文章来源地址https://www.toymoban.com/news/detail-425894.html

到了这里,关于【多线程】CAS的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理

    乐观锁:预测锁竞争不是很激烈。 悲观锁:预测锁竞争会很激烈。 以上定义并不是绝对的,具体看预测锁竞争激烈程度的结论。 轻量级锁加锁解锁开销比较小,效率更高。 重量级锁加锁解锁开销比较大,效率更低。 多数情况下,乐观锁也是一个轻量级锁。 多数情况下,悲

    2024年02月03日
    浏览(34)
  • Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识

    List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。 本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。 关于List的博客文章如下: Java进阶(List)——面试时List常见问题解读 结合源码分析 关于的Set的博

    2024年02月06日
    浏览(45)
  • 单点登录:CAS使用springboot main方法启动cas-server

    1.下载demo 2.使用eclipse导入maven项目。此次我是用的spring tool suite 4导入的。 导入时会用较长时间,10分钟吧。需要下载很多资源。 3.报错,提示缺少jar包 4.其他地方下载该jar包。我是在cas-server-webapp-tomcat-5.3.9.war中解压中找到xmlsectool-2.0.0.jar。 5.安装jar。 6.更新项目。 pom.xml右键

    2024年02月09日
    浏览(45)
  • CAS比较并交换概述

            CAS(Compare And Swap)表示比较并交换,是乐观锁(简单理解为不加锁)的实现,采用的是 自旋锁的思想 。底层是 通过Unsafe类中compareAndSwapInt等方法 实现。         CAS包含三个操作数,分别为:内存值,预估值,更新值。当且仅当内存值=预估值是,才将内存值=更新值

    2024年02月15日
    浏览(35)
  • Java的CAS操作

    技术是为了解决问题而生的,通过 CAS 我们可以以无锁的方式,保证对共享数据进行 “读取 - 修改 - 写回” 操作序列的正确性。 CAS 是乐观锁设计思想的实现。CAS 的思想是:在“读取 - 修改 - 写回”操作序列中,先读取并修改数据,写回数据前先判断读取数据后的这段时间内

    2024年02月05日
    浏览(33)
  • 原子操作CAS

    悲观锁 具有强烈的独占和排他特性。在有悲观锁的情况下,对数据进行处理,数据会处于锁定状态。前面讲到的synchronized同一时间 只允许一个线程访问某块资源 ,其他线程处于阻塞状态,就是一个独占锁,是悲观锁中的一种。 悲观锁适用于写操作比较多的场景。 乐观锁 对

    2024年02月13日
    浏览(26)
  • 【JavaEE】CAS

    作者主页: paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将基础知识一网打尽,希望可以帮到读者们哦。 其他专栏:《MySQL》《C语言》

    2024年01月16日
    浏览(24)
  • 详解CAS算法

    CAS的全称是 Compare And Swap(比较并交换),它是并发编程中的一个重要概念。本文结合Java的多线程操作来讲解CAS算法。 CAS算法的优势是可以在不加锁的情况下保证线程安全,从而减少线程之间的竞争和开销。 目录 一、CAS算法的内容 1、基本思想和步骤 2、CAS伪代码(如果把

    2024年02月16日
    浏览(248)
  • Java——》CAS

    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结——》【SpringBoot】     总结——》【MyBatis、MyBatis-Plus】     总结——》【Linux】     总结——》【MongoDB】    

    2024年02月05日
    浏览(36)
  • CAS + 自旋 锁底层

    为什么会出现多线程安全问题? 在多线程并发下, 假设有 A,B 两个线程同时操作 count = 0 这个公共变量, 在A线程中count++, 在B线程中count++, 正常来说结果应该是 count = 2, 可是同时在A, B两个线程中拿到 count = 0 , 并且都执行count++赋值, 结果就变成了 count = 1 解决办法? 原子操作类: A

    2024年02月12日
    浏览(22)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包