面试官:什么是CAS?存在什么问题?

news2024/11/14 12:14:26

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。


回答

CAS,Compare And Swap,即比较并交换,它一种无锁编程技术的核心机制。其工作方式分为两步:

  1. 比较:它首先会比较内存中的某个值(V)与预期的值(A)是否相等。
  2. **交换:**如果相等,那么会自动将该值(V)更新为新值(B)。如果不相等,不做任何操作。

这个过程是原子操作,保证在并发环境中的数据一致性和线程安全。

CAS 主要存在如下三个问题:

  1. ABA 问题:如果变量 V 的值原先是 A,然后被其他线程改为 B,然后又改回 A,这时CAS操作会误认为自从上次读取以来 V 没有被修改过,从而可能产生错误的操作结果。
  2. 循环时间过长问题CAS操作如果长时间不成功,会不断进行重试,这可能会导致线程长时间处于忙等(Busy-Wait)状态,从而导致 CPU 长时间做无效操作。
  3. 多变量原子问题CAS 只能保证一个变量的原子操作。

详解

CAS 详解

CAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了Java多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是基于 CAS 实现的,可以说CAS 是整个 JUC 的基石。

在CAS中有三个参数:内存值 V、旧的预期值 A以及要更新的值 B。它涉及两个操作:

  1. 比较:首先比较内存位置的当前值 V 和预期原值 A 是否相等,即 V == A ?
  2. 交换:如果相等则将该位置值更新为新值,即 set B → V

用伪代码表示:

if(this.value == A){
    this.value = B
    return true;
}else{
     return false;
}

流程图如下:

CAS 存在的问题

CAS 主要存在如下三个问题:

  1. ABA 问题:如果变量 V 的值原先是 A,然后被其他线程改为 B,然后又改回 A,这时CAS操作会误认为自从上次读取以来 V 没有被修改过,从而可能产生错误的操作结果。
  2. 循环时间过长问题CAS操作如果长时间不成功,会不断进行重试,这可能会导致线程长时间处于忙等(Busy-Wait)状态,从而导致 CPU 长时间做无效操作。
  3. 多变量原子问题CAS 只能保证一个变量的原子操作。
ABA 问题

CAS 需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:一个变量原来的值为 A,后来被某个线程改为 B,再后来又被改回 A。在这种情况下,使用 CAS 进行比较时,会发现变量的值仍然为 A,从而认为这个变量没有被修改过,导致CAS 操作会成功。然而,实际上这个变量经历了 A->B->A的变化,其状态已经发生了变化,可能会导致一些逻辑上的错误。

为了解决 ABA 问题,一种常用的方法是使用版本号,即每次更新变量时,除了改变变量的值,还会更新一个附加的版本号,这样,即使一个变量的值被改回原来的值,它的版本号也会不同。

Java 提供了AtomicStampedReference 来解决 ABA 问题。AtomicStampedReference通过包装[E,Integer] 的元组来对对象标记版本戳stamp,从而避免ABA问题。

AtomicStampedReference 内部有一个 Pair 内部类,它主要用于记录引用和版本戳信息(标识):

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

Pair 是一个不可变对象,其所有属性全部定义为final:

  • reference:即变量当前的值。
  • stamp:版本号,每次变量更新时,版本号也会更新。

同时,Pair 对外提供一个of方法,该方法返回一个新建的Pari对象。

AtomicStampedReference 中对 Pair对象定义为volatile,保证多线程环境下的可见性。

AtomicStampedReferencecompareAndSet()方法源码如下:

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
                expectedReference == current.reference &&
                        expectedStamp == current.stamp &&
                        ((newReference == current.reference &&
                                newStamp == current.stamp) ||
                                casPair(current, Pair.of(newReference, newStamp)));
    }

下面我们将通过一个例子可以可以看到AtomicStampedReferenceAtomicInteger的区别。我们定义两个线程,线程1负责将100 —> 110 —> 100,线程2执行 100 —>120,看两者之间的区别。

public class CASTest {
    public static void main(String[] args) throws InterruptedException {

        //AtomicInteger
        AtomicInteger atomicInteger = new AtomicInteger(100);
        new Thread(() -> {
            System.out.println("线程 atomicInteger_01 开始执行...");
            atomicInteger.compareAndSet(100,110);
            atomicInteger.compareAndSet(110,100);
            System.out.println("线程 atomicInteger_01 已执行完成...");
        }).start();
        // 确保线程1先执行完成
        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            System.out.println("线程 atomicInteger_02 开始执行...");
            System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
        }).start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("========================");

        //AtomicStampedReference
        AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

        new Thread(() -> {
            System.out.println("线程 atomicStampedReference_01 开始执行...");
            int stamp = atomicStampedReference.getStamp();
            atomicStampedReference.compareAndSet(100, 110, stamp, stamp + 1);
            stamp = atomicStampedReference.getStamp();
            atomicStampedReference.compareAndSet(110, 100, stamp, stamp + 1);
            System.out.println("线程 atomicStampedReference_01 已执行完成...");
        }).start();

        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            int tempstamp = atomicStampedReference.getStamp();
            System.out.println("线程 atomicStampedReference_02 开始执行...");
            System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,tempstamp,tempstamp + 1));
        }).start();
    }
}

执行结果:

这是一种正常的写法,我们将 int tempstamp = atomicStampedReference.getStamp(); 移到外面去:

public class CASTest {
    public static void main(String[] args) throws InterruptedException {
         //....

        //AtomicStampedReference
        AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
        int tempstamp = atomicStampedReference.getStamp();
        //...

        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            System.out.println("线程 atomicStampedReference_02 开始执行...");
            System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,tempstamp,tempstamp + 1));
        }).start();
    }
}

执行结果:

循环时间过长问题

CAS 如果长时间操作不成功,则会让 CPU 在那里空转,会增加 CPU 的消耗。针对这个问题我们一般通过减少失败次数和优化重试机制来实现。

  1. 退避策略:在高并发情况下,如果 CAS 操作失败,我们不用立刻就重试,而是让线程暂时“退避”,比如休眠一段时间,这样可以减少竞争线程的数量。如果失败次数增加了,我们可以逐渐增加休眠时间,比如失败小于 10 次,休眠 500毫秒,10~20次休眠 1000 毫秒这样逐步递增。
  2. 限制重试次数:不能让线程一直都在那里重试,我们可以设置一个阈值,超过这个阈值,线程可能需要放弃竞争这个资源,比如报个错之类的。
  3. 自适应:为了应对更加复杂的场景,我们可以采用自适应的策略来动态调整退避时间或重试策略。比如,根据当前系统的负载、CAS 操作的成功率和失败次数等因素,动态调整策略参数。这种方式实现难度比较大,容易采坑。

最后,在一般情况下如果在高并发场景下,其实不是很建议使用 CAS,还不如直接使用锁来的直接,有可能效率会更高。

多变量原子问题

CAS 只能保证一个变量的原子操作。对于多个变量是无法做到原则操作的。我们一般有如下几种方案:

  1. 使用锁:这是最直接的方案,直接放弃 CAS,采用锁机制来解决多变量原则问题。
  2. 使用 AtomicReference:将多个变量封装成一个 Java 对象,然后使用 AtomicReference 对这个对象进行原子操作。

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

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

相关文章

汉字转拼音工具类

一&#xff0c;汉字转成拼音大写首字母 public static String chineseToPinyin(String chinese) {//创建一个 StringBuilder 对象用于存储转换后的拼音。StringBuilder pinyin new StringBuilder();//创建一个汉语拼音输出格式对象。HanyuPinyinOutputFormat format new Han…

Redis-01 入门和十大数据类型

Redis支持两种持久化方式&#xff1a;RDB持久化和AOF持久化。 1.RDB持久化是将Redis的数据以快照的形式保存在磁盘上&#xff0c;可以手动触发或通过配置文件设置定时触发。RDB保存的是Redis在某个时间点上的数据快照&#xff0c;可以通过恢复RDB文件来恢复数据。 2.AOF持久化…

MySQL 中的 EXPLAIN 命令:洞察查询性能的利器

《MySQL 中的 EXPLAIN 命令&#xff1a;洞察查询性能的利器》 在 MySQL 数据库的使用中&#xff0c;优化查询性能是至关重要的一项任务。而 EXPLAIN 命令就是我们用来深入了解查询执行计划的强大工具。今天&#xff0c;我们就来一起探讨如何在 MySQL 中使用 EXPLAIN 命令&…

数据结构-3.2.栈的顺序存储实现

一.顺序栈的定义&#xff1a;top指针指向栈顶元素 1.图解&#xff1a; 2.代码&#xff1a; #include<stdio.h> #define MaxSize 10 //定义栈最多存入的元素个数 ​ typedef struct {int data[MaxSize]; //静态数组存放栈中元素int top; //栈顶指针 } SqStack; ​ int…

python mysql pymysql 数据库操作,常用脚本,个人小工具

起因&#xff0c; 目的: 整理 mysql 工具 启动数据库 检查服务器是否启动了: Get-Service -Name ‘mysql*’ 如果没启动的话&#xff0c;那么就启动: net start MySQL80 (最好是开启管理员权限) 1, 日常最常用的&#xff0c;创建连接 --> 查看所有数据库 —> 查看所有…

预处理、makefile、静动态库编写、nfs挂载、快捷命令

c查看预处理后的文件 查看执行后的汇编代码 预处理过程 静态库和动态库 静态库编写 实践 a 动态库生成 查找文件命令 动态库升级 链接的库找不到 命名要为linfun.so 执行时-lfun才能找到 系统会将lfun补充成libfun查找&#xff08;系统默认路径/user/lib/...&#xff09; 链…

C++:string 类详解

目录 简介 使用 初始化(构造函数、拷贝构造函数) 析构函数 赋值运算符重载(operator) 成员常量(npos) 运算符重载[ ](operator[ ]) size() 和 length() 迭代器( begin() 和 end() ) 范围 for 迭代器和范围 for 的比较 反向迭代器( rbegin() 和 rend() ) const 迭…

每日刷题(算法)

我们N个真是太厉害了 思路&#xff1a; 我们先给数组排序&#xff0c;如果最小的元素不为1&#xff0c;那么肯定是吹牛的&#xff0c;我们拿一个变量记录前缀和&#xff0c;如果当前元素大于它前面所有元素的和1&#xff0c;那么sum1是不能到达的值。 代码&#xff1a; #def…

elasticsearch实战应用

Elasticsearch(ES)是一种基于分布式存储的搜索和分析引擎&#xff0c;目前在许多场景得到了广泛使用&#xff0c;比如维基百科和github的检索&#xff0c;使用的就是ES。本文总结了一些使用心得体会&#xff0c;希望对大家有所帮助。 一、技术选型 说到全文搜索大家肯定会想到…

软件测试 BUG 篇

目录 一、软件测试的生命周期 二、BUG 1. bug的概念 2. 描述bug的要素 3. bug的级别 4. bug的生命周期 5. 与开发产生争执怎么办&#xff1f;&#xff08;面试高频考题&#xff09; 5.1 先检查自身&#xff0c;是否bug描述不清楚 5.2 站在用户角度考虑并抛出问题 5.3 …

[vue2+axios]下载文件+文件下载为乱码

export function downloadKnowledage(parameter) {return axios({url: /knowledage/download,method: GET,params: parameter,responseType: blob}) }添加 responseType: blob’解决以下乱码现象 使用触发a标签下载文件 downloadKnowledage(data).then((res) > {let link …

PHP及Java等其他语言转Go时选择GoFly快速快速开发框架指南

概要 经过一年多的发展GoFly快速开发框架已被一千多家科技企业或开发者用于项目开发&#xff0c;他的简单易学得到其他语言转Go首选框架。且企业版的发展为GoFly社区提供资金&#xff0c;这使得GoFly快速框架得到良好的发展&#xff0c;GoFly技术团队加大投入反哺科技企业和开…

模版进阶(template)

1.非类型模版参数 模版参数分类类型形参与非类型形参。 ① 类型形参&#xff1a;出现在在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 ② 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当…

Java键盘输入语句

编程输入语句 1.介绍:在编程中&#xff0c;需要接受用户输入的数据&#xff0c;就可以使用键盘输入语句来获取。 2.步骤&#xff1a; 1&#xff09;导入该类的所在包&#xff0c;java.util.* 2)创建该类对象&#xff08;声明变量&#xff09; 3&#xff09;调用里面的功能 3…

[2025]医院健康陪诊系统(源码+定制+服务)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

计算机毕业设计 奖学金评定管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

MySQL ------- 索引(B树B+树)

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;希望本文内容能帮到你&#xff01;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 目录 一&#xff1a;索引的特点 二&#xff1a;索引适用的场景 三&#xff1a;MySQL中索引操作 1&#xff1a;…

LTE SSS辅同步信号检测(相关法)

本文介绍一下SSS检测原理,本文采用联合检测算法,用复杂度来换取性能,适合工程上使用,SSS信号的产生往期已经介绍过了,这里就不介绍了。 1 SSS两个序列采用Interleaved结构而没有采用Localized集中式的原因是他比集中式可以获得更大的频率分集和干扰随机化效果。 2 根据3…

Python 从入门到实战22(类的定义、使用)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了面向对象简单介绍相关知识。今天我们将学习一…

828华为云征文 | 深度评测,华为云Flexus X实例在Sysbench性能测试中的亮眼表现

前言 本文章评测了华为云Flexus X实例在Sysbench性能测试中的亮眼表现。Flexus X凭借其新一代处理器和智能算力技术&#xff0c;在CPU、内存、磁盘I/O及网络性能上均展现出了卓越的能力。通过Sysbench的详尽测试&#xff0c;Flexus X实例在多核计算能力、内存吞吐量、磁盘响应速…