详解CAS算法

news2024/11/25 2:03:05

CAS的全称是 Compare And Swap(比较并交换),它是并发编程中的一个重要概念。本文结合Java的多线程操作来讲解CAS算法。

CAS算法的优势是可以在不加锁的情况下保证线程安全,从而减少线程之间的竞争和开销。

目录

一、CAS算法的内容

1、基本思想和步骤

2、CAS伪代码(如果把CAS想象成一个函数)

二、CAS算法的应用

1、实现原子类

*伪代码实现原子类

2、实现自旋锁

*伪代码实现自旋锁

三、CAS的ABA问题

1、ABA问题引发的BUG

2、解决ABA问题——使用版本号


一、CAS算法的内容

1、基本思想和步骤

CAS算法的基本思想是,先比较内存M中的值与寄存器A中的值(旧的预期值,expectValue)是否相等,如果相等,则将寄存器B中的值(新值,swapValue)写入内存;如果不相等,则不做任何操作。整个过程是原子的,不会被其他并发操作中断。

这里虽然涉及到内存与寄存器值的“交换”,但更多时候我们并不关心寄存器中存的值是什么,而是更关心内存中的数值(内存中存的值即变量的值)。所以这里的“交换”可以不用看成交换,直接看成是赋值操作就行,即把寄存器B中的值直接赋值到内存M中了。

CAS 操作包括三个操作数:一个内存位置(通常是共享变量)、期望的值和新值。CAS 操作的执行过程如下:

  1. 读取内存位置的当前值。
  2. 比较当前值与期望的值是否相等。
  3. 如果相等,将新值写入内存位置;否则,操作失败。

2、CAS伪代码(如果把CAS想象成一个函数

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

上面给出的代码只是伪代码,并不是真实的CAS代码。事实上,CAS操作是一条由CPU硬件支持的、原子的硬件指令,这一条指令就能完成上述这一段代码的功能。

“一条指令”和“一段代码”的最大区别在于原子性。上述这一段伪代码不是原子的,运行过程中可能随着线程的调度有概率产生线程安全问题;但原子的指令不会有线程安全问题。

同时,CAS也不会有内存可见性的问题,内存可见性相当于是编译器把一系列指令的进行了调整,把读内存的指令调整成读寄存器的指令。但CAS本身就是指令级别读取内存的操作,所以不会有内存可见性带来的线程不安全问题。

因此,CAS可以做到不加锁也能一定程度地保证线程安全。这样就引申出基于CAS算法的一系列操作。


二、CAS算法的应用

CAS可以用于实现无锁编程。实现原子类和实现自旋锁就是无锁编程的其中两个方式。

1、实现原子类

标准库 java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令而不是使用锁) 来保证其他操作的原子性

例如Atomiclnteger 类提供了方法 incrementAndGet、getAndIncrement 和 decrementAndGet、getAndDecrement,它们分别以原子方式将一个整数自增或自减。

        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
                //num++
                num.getAndIncrement();
                //++num
                num.incrementAndGet();
                //num--
                num.getAndDecrement();
                //--num
                num.decrementAndGet();
        });

例如,可以安全地生成数值序列,如下所示

import java.util.concurrent.atomic.AtomicInteger;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //num++
                num.getAndIncrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //num++
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(num.get());
    }
}

运行结果:最终num的值正好是100000

这是因为 getAndIncrement() 方法以原子的方式获得 num 的值,并将 num 自增也就是说, 获得值、 1 并设置然后生成新值的操作不会中断可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值

通过查看源码可以发现,getAndIncrement() 方法并没有用到加锁(synchronized):

但再进入 getAndAddInt 方法可以发现,其中用到了 CAS 算法:

再进入 compareAndSwapInt 方法后会发现,这是一个由 native 修饰的方法。CAS算法的实现依赖于底层硬件和操作系统提供的原子操作支持,因此它是更偏向底层的操作。 

补充 - 与之形成对比的线程不安全的案例是:

下面就是一个线程不安全的例子。该代码中创建了一个counter变量,同时分别创建了两个线程t1和t2,让这两个线程针对同一个counter令其自增5w次。

class Counter {
    private int count = 0;
 
    //让count增加
    public void add() {
        count++;
    }
 
    //获得count
    public int get() {
        return count;
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
 
        // 创建两个线t1和t2,让这两个线程分别对同一个counter自增5w次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
 
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
 
        t1.start();
        t2.start();
 
        // main线程等待两个线程都执行结束,然后再查看结果
        t1.join();
        t2.join();
 
        System.out.println(counter.get());
    }
}

按理来说,最终输出counter的结果应当是10w次。但我们运行程序后发现,不但结果不是10w,而且每次运行的结果都不一样——实际结果看起来像一个随机值。

由于线程的随即调度,count++这一语句并不是原子的,它本质上是由3个CPU指令构成:

  1. load。把内存中的数据读取到CPU寄存器中。
  2. add。把寄存器中的值进行 +1 运算。
  3. save。把寄存器中的值写回到内存中。

CPU需要分三步走才能完成这一自增操作。如果是单线程中,这三步没有任何问题;但在多线程编程中,情况就会不同。由于多线程调度顺序是不确定的,实际执行过程中,这俩线程的count++操作的指令排列顺序会有很多种不同的可能:

上面只列出了非常小的一部分可能,实际中有更多可能的情况。而不同的排列顺序下,程序执行的结果可能是截然不同的!比如以下两种情况的执行过程:

因此, 由于实际中线程的调度顺序是无序的,我们并不能确定这俩线程在自增过程中经历了什么,也不能确定到底有多少次指令是“顺序执行”的,有多少次指令是“交错执行”的。最终得到的结果也就成了变化的数值,count一定是小于等于10w的

(来自文章:Java多线程基础-6:线程安全问题及解决措施) 

*伪代码实现原子类

代码:

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

上面代码中,虽然看似刚刚把 value 赋值给 oldValue 后,就紧接着比较 value 和 oldvalue是否相等,但比较结果依然是可能不相等的。因为这是在多线程的环境下。value 是成员变量,如果两个线程同时调用 getAndIncrement 方法,就有可能出现不相等的情况。其实此处的 CAS 就是在确认当前 value 是否变过。如果没变过,才能自增;如果变过了,就先更新值,再自增。

之前的线程不安全,有一个很大的原因就是一个线程不能及时感知到另一个线程对内存的修改:

之前线程不安全,是因为t1在自增的时候先读后自增。此时在t1自增之前,t2已经自增过了,t1是却还是在一开始的0的基础上自增的,此时就会出现问题。

但CAS操作使得t1在执行自增之前,先比较一下寄存器和内存中的值是否一致,只有一致了才执行自增,否则就重新将内存中的值向寄存器同步一下。

这个操作不涉及阻塞等待,因此会比之前加锁的方案快很多。

2、实现自旋锁

自旋锁是一种忙等待锁的机制。当一个线程需要获取自旋锁时,它会反复地检查锁是否可用,而不是立即被阻塞。如果获取锁失败(锁已经被其他线程占用),当前线程会立即再尝试获取锁,不断自旋(空转)等待锁的释放,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。这样能保证一旦锁被其他线程释放,当前线程能第一时间获取到锁。一般乐观锁的情况下(锁冲突概率低),实现自旋锁是比较合适的。

*伪代码实现自旋锁

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


三、CAS的ABA问题

CAS的ABA问题是使用CAS时遇到的一个经典的问题。

已知 CAS 的关键是对比内存和寄存器中的值,看二者是否相同,就是通过这个比较来判断内存中的值是否发生过改变。然而,万一对比的时候是相同的,但其实内存中的值并不是没变过,而是从A值变成B值后又变回了A值呢?

此时,有一定概率会出问题。这样的情况就叫做ABA问题。CAS只能对比值是否相同,但不能确定这个值是否中间发生过改变。

这就好比我们从某鱼上买了一个手机,但无法判定这个手机是刚出厂的新手机,还是别人用旧过了之后又翻新过的翻新机。

1、ABA问题引发的BUG

其实大部分的情况下ABA问题影响并不大。但是不排除一些特殊情况:

假设小明有 100 存款。他想从 ATM 取 50 块钱。取款机创建了两个线程,并发地来执行 -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 问题引起的。

2、解决ABA问题——使用版本号

ABA 问题的关键是内存中共享变量的值会反复横跳。如果约定数据只能单方向变化,问题就迎刃而解了。

由此引入“版本号” 这一概念。约定版本号只能递增(每次修改变量时,都会增加一个版本号)。而且每次 CAS 在对比的时候,对比的就不是数值本身,而是对比版本号。这样其他线程在进行 CAS 操作时可以检查版本号是否发生了变化,从而避免 ABA 问题的发生。

(以版本号为基准,而不以变量数值为基准。约定了版本号只能递增,就不会出现ABA这样的反复横跳问题。)

不过在实际情况中,大部分情况下即使遇到了ABA问题,也没有什么关系。知晓版本号可以用来解决ABA问题即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/754018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

979. 在二叉树中分配硬币(力扣)

在二叉树中分配硬币 题目一题一解&#xff1a;DFS(java)思路步骤解析测试代码复杂度分析运行结果 优化代码思路测试代码运行结果复杂度分析 题目 给你一个有 n 个结点的二叉树的根结点 root &#xff0c;其中树中每个结点 node 都对应有 node.val 枚硬币。整棵树上一共有 n 枚…

韦东山-BeagleV-Ahead TH1520 RISC-V 高性能开发板开箱硬件评测

BeagleV-Ahead开箱硬件讲解 BeagleV-Ahead开箱介绍 非常感谢 中国杭州平头哥半导体优先公司 和 Beagle社区给予我们 全球首款高性能 TH1520 RISC-V SBC开发板 BeagleV-Ahead评测工作&#xff0c;我们将围绕 开发板 外观 &#xff0c;板载功能接口&#xff0c;系统启动系统初体…

AcWing 107. 超快速排序—逆序对

问题链接: AcWing 107. 超快速排序 问题描述 分析 这道题考查的算法不难&#xff0c;就只是利用归并排序来求逆序对的数量&#xff0c;但是主要是如何分析问题&#xff0c;如何能从问题中看出来和逆序对数量有关&#xff0c;现在的题目基本上很少是那种模板算法题了&#xff…

力扣21. 合并两个有序链表

题目 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 链接&#xff1a;21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09; 题解 设置两个指针head和tail&#xff0c;head用来指向新链表的头结点&#xff0c;tail…

HCIA初级考试题目(复制粘贴命令修改满分试卷)

要求&#xff1a; &#xff08;设备名称按照拓扑标识修改&#xff0c;注意区分大小写&#xff09; 1、ISP路由器仅配置IP地址 2、test-1和test-2仅作为代替终端设备进行测试使用&#xff0c;路由采用静态路由 3、R1/R2之间使用OSPF做到内网全通&#xff0c;单区域&#xff0c;O…

3.6.cuda运行时API-共享内存的学习

目录 前言1. 共享内存2. shared memory案例3. 补充知识总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习精简 CUDA 教程-共享…

前端vue入门(纯代码)32_编程式路由导航

星光不问赶路人&#xff0c;时光不负有心人 【30.Vue Router--编程式导航】 除了使用 <router-link> 创建 a 标签来定义导航链接&#xff0c;我们还可以借助 $router 的实例方法&#xff0c;通过编写代码来实现。 编程式路由导航的5种方法 我们先看一下组件实例中的t…

掌握这几招,让你的CTA按钮更吸引人点击

CTA全称Call-To-Action&#xff0c;是行为召唤按钮&#xff0c;是App和网页设计中的关键元素。 在落地页设计制作中&#xff0c;CTA按钮是用户在访问页面后引导用户去点击并且跳转至下一个流程&#xff08;如购买、联系、提交等行为&#xff09;的按钮控件。其核心目标是引导用…

函数图形化显示练习(进阶)

运行代码: //函数图形化显示练习&#xff08;进阶&#xff09; #include"std_lib_facilities.h" #include"GUI/Simple_window.h" #include"GUI/GUI.h" #include"GUI/Graph.h" #include"GUI/Point.h" //定义函数 double one…

【Spring Cloud系列】Hystrix应用详解

【Spring Cloud系列】Hystrix应用详解 文章目录 【Spring Cloud系列】Hystrix应用详解一、概述二、什么是Hystix三、Hystrix作用四、Hystrix设计原则五、Hystrix实现原理5.1 隔离5.2 熔断5.3 降级服务降级主要用于什么场景呢实现服务降级需要考虑几个问题降级分类 5.4 缓存请求…

使用Hugging Face预训练Bert处理下游任务显存占用过多

在使用HuggingFace的transformer下的BertForMaskedLM进行预训练语言模型的load时&#xff0c;bert会占用很大的显存。 这里可以考虑使用TinyBERT&#xff0c;速度和显存上都能得到很大的优化。 具体的方法进入https://huggingface.co/huawei-noah/TinyBERT_General_4L_312D/tr…

day01——项目导入+环境搭建

目录 软件开发整体介绍 软件开发流程 需求分析 设计阶段 编码阶段 测试阶段 上线运维 角色分工 软件环境 苍穹外卖项目介绍 项目介绍 功能架构 产品原型 ​编辑 技术选型 开发环境搭建——前端环境搭建 开发环境搭建——后端环境搭建 熟悉项目结构 ​编辑 使…

入门开发教程之网站品质教程

目录 网站品质 教程 网站品质教程 背景 要素 可访问性 可用性 可靠性 可维护性 提升网站品质 针对性调整 优化网页速度 提供多种访问方法 结论 网站品质教程 背景 在今天这个数字化时代&#xff0c;网站已经成为了各个行业展示产品和服务的重要媒介。而网站品质是…

百分点科技苏萌受邀出席首届全国统计与数据科学联合会议

7月11-13日&#xff0c;首届全国统计与数据科学联合会议在北京举行&#xff0c;会议由中国现场统计研究会、中国数学会概率统计分 会、全国工业统计学教学研究会和中国商业统计学会联合主办&#xff0c;北京大学统计科学中心承办&#xff0c;旨在促进统计与数据科学领域发展&a…

H3C-Cloud Lab实验-IPv6实验

实验拓扑图&#xff1a; 实验需求&#xff1a; 1、在R1和PC3上开启IPv6链路本地地址自动生成&#xff0c;测试是否能够使用链路本地地址互通 2、为R1配置全球单播地址2001::1/64&#xff0c;使PC3能够自动生成与R1同一网段的IPv6地址3、测试R1和PC3是否能够使用全球单播地址互…

【UE4 塔防游戏系列】05-制作可跟踪旋转的炮塔

目录 效果 步骤 一、设置游戏观察视角 二、设置PlayerController 三、制作可跟踪旋转的炮塔 效果 步骤 一、设置游戏观察视角 在视口中调整好位置&#xff0c;能够看到敌人行走的全部路线即可。然后在此处创建CameraActor 打开关卡蓝图&#xff0c;设置使用这个相机的…

【Kaggle】初学者几个冷门的操作总结

文章目录 一、如何看当前的目录&#xff1f;二、Kaggle如何切换路径&#xff1f;三、与包安装或设置有关的错误四、如何把 Kaggle 上的 input 数据转到 output 中&#xff1f; 一、如何看当前的目录&#xff1f; 在 Linux 中&#xff0c;你可以使用 pwd 命令来查看当前所在的目…

UML与SYSML的关系

UML与SysML的联系 UML&#xff08;统一建模语言&#xff09;和SysML&#xff08;系统建模语言&#xff09;是两种与建模相关的语言&#xff0c;它们之间存在联系和区别。 SysML的图分类如下图所示。 联系 SysML是基于UML的&#xff0c;它重用了UML 2的子集&#xff0c;并提…

MySQL持久化数据——主从分离 Linux下创建2个MySQL的Docker容器 挂载方式启动 配置主从

目录 引出数据库的事务1.原子性2.一致性3.隔离性4.持久性 MySQL持久化数据0.在宿主机centos创建主的文件夹1.拷贝my.cnf配置文件2.挂载方式启动主mysql3.修改my.cnf文件的权限【bug】mysql: [ERROR] unknown variable server-id200. 3.修改主的my.cof文件4.创建主从账号slave5.…

Mysql索引实战

Mysql索引实战 一&#xff1a;概述1.1 索引如何提高查询效率&#xff1a; 二&#xff1a;结构2.1 主要索引结构2.2 详解BTree2.2.1 二叉树2.2.2 红黑树2.2.3 B-Tree2.2.4 BTree2.2.5 为什么InnoDB存储引擎选择使用Btree索引结构&#xff1f; 三&#xff1a;索引分类3.1 按照作用…