什么是CAS/CAS的应用/CAS的ABA问题

news2024/11/26 12:51:11

文章目录

  • CAS
    • 1. 什么是CAS
    • 2. CAS的应用
      • 2.1 实现原子类
      • 2.2 实现自旋锁
    • 3. CAS的ABA问题
      • 3.1 什么是ABA问题
      • 3.2 ABA问题引来的BUG
      • 3.3 解决方案

CAS

1. 什么是CAS

CAS: 全称Compare and swap, 字面意思:”比较并交换“. 操作:

设V为内存中的值, A为寄存器中的值(旧的预期值), B也为寄存器中的值(需要修改的新值)

  1. 比较V和A是否相等(比较)
  2. 如果相等, 则将B赋给V, 返回true(交换)
  3. 如果不相等, 返回false

CAS伪代码

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

CAS是一个原子的CPU指令完成的, 这个指令不可拆分, 上述伪代码不是原子的.

它既然是原子的, 那么我们编写线程安全的代码时, 就可以借助CAS

所以, 基于CAS又能衍生出一套"无锁编程"

但是CAS的适用范围具有一定的局限性, 只能针对一些特定场景进行优化.

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

2. CAS的应用

2.1 实现原子类

我们在前文讲解线程安全问题时, 曾举过一个例子, 就是多个线程针对一个count变量++, 如果不进行加锁, 那么就会引发线程安全问题. 当时我们是使用的synchronized解决的问题.

其实我们还可以使用原子类, 可以更高效更简单的操作.

标准库中提供了java.util.concurrent.atomic包, 里面的类都是基于CAS来实现的原子类.

在这里插入图片描述

AtomicInteger, AtomicLong提供了自增/自减/自增任意值/自减任意值. 这些操作就是基于CAS按照无锁编程的方式实现.

//例子
public class Demo26 {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //count++
                count.getAndIncrement();
//                //++count
//                count.incrementAndGet();
//                //count--
//                count.getAndDecrement();
//                //--count
//                count.decrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();

            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.get());
    }
}

伪代码实现:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        //这里的比较, 为的是检查当前value是否被其他线程改变了.
        //如果相等, 证明没有被改变, 操作还是原子的
        //如果不相等, 证明被其他线程改变了, 此时要立即重新读取内存的值, 然后再次比较相等...
        while ( CAS(value, oldValue, oldValue+1) != true) {
            //如果value!=oldvalue, 那么就更新oldvalue, 继续CAS
            oldValue = value;
        }
        return oldValue;
    }
}

当两个线程并发的进行++操作时:

  • 如果不加任何限制, 会引发线程安全问题
  • 加锁保证线程安全: 通过锁, 强制两个++操作串行执行
  • 原子类/CAS保证线程安全: 借助CAS来识别当前是否出现了并发/穿插的情况.
    • 如果没穿插, 直接修改, 是安全的
    • 如果穿插了, 就重新读取内存中最新的值, 再次尝试修改.

2.2 实现自旋锁

自旋锁伪代码

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

3. CAS的ABA问题

3.1 什么是ABA问题

ABA 的问题:

假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.

接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

  • 先读取 num 的值, 记录到 oldNum 变量中.

  • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这个时候 t1 究竟是否要更新 num 的值为 Z 呢?

到这一步, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程.

在这里插入图片描述

3.2 ABA问题引来的BUG

账户里面有100元, 要取款50

假设出现了极端情况

摁第一次取款的时候, 卡了一下, 然后又摁了一次

这样便产生了两个"取款请求", ATM使用两个线程来处理这两个请求

假设按照CAS的方式进行取款, 每个线程这样操作:

  1. 读取账户余额, 放到变量M中
  2. 使用CAS判定当前实际余额是否是M, 如果是就把实际余额改成M-50, 如果不是, 放弃操作

正常的过程

  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 相同, 再次执行扣款操作

这个时候, 扣款操作被执行了两次

t1扣款50, t3又增加50, 就让t2错误的认为没有线程修改过余额, 就又扣款了50.

这就是A-B-A问题产生的后果

3.3 解决方案

我们可以给要修改的值, 引入版本号, 让它只增长不减小.在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

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

  • 真正修改的时候,

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

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

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

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

相关文章

无人零售已成为新兴趋势

无人零售已成为新兴趋势 在新零售浪潮中&#xff0c;必然会涌现新的商业形态&#xff0c;而无人零售则是其中典型代表之一。传统零售受制于人力和场地等限制&#xff0c;消费者体验较差&#xff0c;如长时间排队、缓慢结账、距离过远等问题。而无人零售解决方案&#xff0c;包括…

面试官:java如何实现线程间通信?

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

PyQt6 QLineEdit单行文本框控件

​锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计21条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话…

一起学docker系列之十docker安装tomcat实践

目录 前言1 安装tomcat的步骤步骤 1: 查找并拉取 Tomcat 镜像步骤 2: 运行 Tomcat 容器步骤 3: 管理 Tomcat 容器步骤 4: 访问 Tomcat 首页 2 解决访问首页的404访问不到的问题2.1 Tomcat 10 的默认设置问题2.2 端口映射或防火墙问题 3 推荐使用 Tomcat 8.5 版本总结 前言 当安…

2.前端--HTML标签基本概念【2023.11.25】

1.基本语法规范 HTML 标签是由尖括号包围的关键词&#xff0c;例如 <html>。HTML 标签通常是成对出现的&#xff0c;例如 和 &#xff0c;我们称为双标签。有些特殊的标签必须是单个标签&#xff08;极少情况&#xff09;&#xff0c;例如 <br />我们称为单标签。 …

【挑战业余一周拿证】一、亚马逊云科技简介 - 第 3 节 - 云计算

第 3 节 - 云计算 在深入了解亚马逊云科技的各个部分之前&#xff0c;让我们先缩小视野&#xff0c;对云进行一个合理的定义。云计算就是通过互联网按需提供 IT 资源并采用按需付费定价模式&#xff0c;下面&#xff0c;我们将进行详细说明。 按需提供表示的是亚马逊云科技会在…

2024年最新最全的Jmeter接口测试必会技能:jmeter_HTTP Cookie管理器

HTTP Cookie管理器 HTTP Cookie管理器可以像浏览器一样自动存储和发送cookie&#xff0c;以这种自 动收集的方式收集到的cookie不会在cookie manager中进行展示&#xff0c;但是运行后&#xff0c; 可以通过 查看结果树&#xff08;监听器&#xff09;可以查看到cookie信息 除…

RandomAccessFile学习笔记

文章目录 RandomAccessFile学习笔记前言1、RandomAccessFile基本介绍1.1 RandomAccessFile相关基本概念1.2 RandomAccessFile家族体系 2、RandomAccessFile基本使用2.1 RandomAccessFile常用API介绍2.2 RandomAccessFile常用API演示2.3 RandomAccessFile实现断点续传 1、Random…

Centos Bind安装与排错

1.配置Centos系统静态IP vi/etc/sysconfig/network-scripts/ifcfg-ens33BOOTPROTOstaticIPADDR192.168.1.100NETMASK255.255.255.0GATEWAY192.168.1.1DNS18.8.8.8:wqsudo systemctl restart network.service 2.安装BIND&#xff08;需要服务器连接互联网&#xff0c;如果服务…

C/C++内存管理,malloc,realloc,calloc,new,delete详解!!!

1.初步了解内存中各个区间存储的数据特征 1.栈区&#xff1a;存储一些局部变量、函数参数、返回值等&#xff0c;跟函数栈振有关&#xff0c;出了作用域&#xff0c;生命周期结束。 2.堆区&#xff1a;用于动态开辟空间&#xff0c;如果不主动销毁空间&#xff0c;则程序运行结…

保护模式进阶

本系列文章只做个人学习记录使用 参考资料&#xff1a; 《操作系统真象还原》 从0到-1写一个操作系统 获取物理内存容量 计算机要想被使用&#xff0c;就必须先管理&#xff0c;我们想和物理内存打交道&#xff0c;就必须先知道物理内存有多大 linux获取内存的方法 在linux…

计算机组成原理4

1.汇编语言 2.汇编语言常见的运算指令 3.AT&T格式 和 Intel格式 4.跳转指令 5.cmp比较的底层原理 6.函数调用的机器级表示 7.CISC和RISC

公司人事管理系统

1.问题描述 一个小公司包含四类人员&#xff1a;经理&#xff0c;技术人员&#xff0c;销售人员和销售经理&#xff0c;各类人员的工资计算方法如下&#xff1a;经理&#xff1a;固定月薪&#xff08;8000&#xff09;&#xff1b;技术人员&#xff1a;月薪按技术等级&#xf…

【深度学习笔记】05 线性回归

线性回归 线性回归基于几个简单的假设&#xff1a; 首先&#xff0c;假设自变量 x \mathbf{x} x和因变量 y y y之间的关系是线性的&#xff0c; 即 y y y可以表示为 x \mathbf{x} x中元素的加权和&#xff0c;这里通常允许包含观测值的一些噪声&#xff1b; 其次&#xff0c;我…

Spring Cloud Gateway 的简单介绍和基本使用

前言 本文主要对Spring Cloud Gateway进行简单的概念介绍&#xff0c;并通过多模块编程的方式进行一个简单的实操。 文章目录 前言1 什么是网关&#xff08;概念&#xff09;2 微服务中的网关2.1 问题12.2 问题2 3 网关作用4 Spring Cloud Gateway组成5 Spring Cloud Gateway基…

51代码审计-PHP框架MVC类上传断点调试

知识点1&#xff0c;文件上传漏洞挖掘 搜索关键字$_FILES phpmvc架构 MVC模式&#xff08;Model-View-Controller&#xff09;是软件工程中的一种软件架构模式。 MVC把软件系统分为三个基本部分&#xff1a;模型&#xff08;Model&#xff09;、视图&#xff08;View&#…

5.前端--CSS-基本概念【2023.11.26】

1. CSS 语法规范 CSS 规则由两个主要的部分构成&#xff1a;选择器以及一条或多条声明。 属性和属性值之间用英文“:”分开 多个“键值对”之间用英文“;”进行区分 选择器 : 简单来说&#xff0c;就是选择标签用的。 声明 &#xff1a;就是改变样式 2.CSS引入方式 按照 CSS 样…

【C++】多线程(一):std::thread的使用

这篇文章应我朋友的邀请&#xff0c;写一篇文章介绍下C多线程。 编译环境准备 首先确定你的编译器支持std的thread&#xff0c;如果不支持&#xff0c;就会出现诸如“thread找不到”的问题。 以下假设你使用 gnu gcc 编译器&#xff0c;因为 MSVC 的我也不太熟悉。 linux …

【详解二叉树】

&#x1f320;作者&#xff1a;TheMythWS. &#x1f387;座右铭&#xff1a;不走心的努力都是在敷衍自己&#xff0c;让自己所做的选择&#xff0c;熠熠发光。 目录 树形结构 概念 树的示意图 树的基本术语 树的表示 树的应用 二叉树(重点) 二叉树的定义 二叉树的五…

实战oj题——用队列实现栈

前言&#xff1a;Leetcode栈和队列的习题&#xff0c;用两个队列实现栈。 【由于我们是用C语言完成这道题&#xff0c;所以我们要将关于队列的实现代码插入到题中&#xff0c;在创建一个栈&#xff0c;栈里包含两个队列。】 思路&#xff1a;我们用两个队列来实现&#xff0c;因…