【多线程】详解 CAS 机制

news2024/11/24 14:42:35

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述

文章目录

  • 1. CAS 是什么
    • 1.1 CAS 具体步骤
    • 1.2 CAS 伪代码
  • 2. CAS 的应用
    • 2.1 实现原子类
      • 2.1.1 AtomInteger 类
      • 2.1.2 伪代码实现原子类
    • 2.2 实现自旋锁
      • 2.2.1 自旋锁是什么
      • 2.2.2 伪代码实现自旋锁
  • 3. CAS 的 ABA 问题
    • 3.1 ABA 问题
    • 3.2 ABA 问题引起的 BUG
    • 3.2 ABA 问题的解决方案 —— 使用版本号

1. CAS 是什么

CAS】全称为 Compare and swap,即"比较并交换",相当于通过一个原子操作,同时完成"读取内存,比较是否相等,修改内存"这三个步骤,本质上需要 CPU 指令支持~

CAS 是并发编程中一个重要的概念,相当于是打开了新世界的大门,可以在不加锁的情况下保证线程安全,从而减少线程之间的竞争和开销,通常用于无锁编程,本文将结合 Java 多线程操作讲解 CAS 机制,我们一起来看看吧!

1.1 CAS 具体步骤

CAS 机制的基本思想是,先比较内存 V 中的值与寄存器 A 中的值(旧的预期值),是否相等,如果相等,则将寄存器 B 中的值(需要修改的新值)写入内存中,如果不相等,则不作任何操作,这整个过程是原子的~

CAS 涉及到以下三个操作,假设内存中的原数据V,旧的预期值A,需要修改的新值B

  • 读取内存值:将需要修改的值从主内存中读入本地线程缓存或工作内存
  • 比较并尝试交换:比较 A 与 V 是否相等,如果比较相等,将 B 写入 V,如果不相等,不作任何操作
  • 返回操作结果:如果成功更新了值,则返回成功标志或新值,如果失败更新,则返回失败标志或当前内存中的值

1.2 CAS 伪代码

如果把 CAS 想象成一个函数,可以得到 CAS 的伪代码,但是下述的伪代码,并不是真正的 CAS 代码,事实上,CAS 操作是一条由 CPU 硬件支持、原子的硬件指令,而这一条指令就可以完成下述这一段代码的功能(CAS 本身就是对应一条 CPU 指令,不可拆分的最小单位,此时,CAS 中比较和交换动作是没办法再拆分的)

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

图解如下:
在这里插入图片描述
可以知道,上述这一段代码,非原子,运行过程中可能随着线程的调度有概率产生线程安全问题,而原子指令不会有线程安全问题~

同时,CAS也不会有内存可见性的问题,内存可见性是编译器把一系列指令进行调整,把读内存指令调整成直接读寄存器的指令,效率大大提升,可能会误判,从而产生 bug,但是 CAS 本身就是指令级别读取内存的操作,因此,不会有内存可见性带来的线程安全问题

CAS 可以不加锁也能一定程度保证线程安全!这样就可以基于 CAS 机制,实现一系列操作

2. CAS 的应用

2.1 实现原子类

2.1.1 AtomInteger 类

标准库在 java.util.concurrent.atomic 包里提供很多类使用高效的指令来保证操作的原子性,而不是使用加锁来保证,其中提供 AtomInteger 类,能够以原子方式保证一个整数自增或自减操作的线程安全~

AtomInteger 类提供如下 4 个方法:

 getAndIncrement(); //后置++
 incrementAndGet();	//前置++
 getAndDecrement();	//后置--
 decrementAndGet();	//前置--
public class ThreadDemo {
    public static void main(String[] args) {
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            //num++
            num.getAndIncrement();
            System.out.println(num.get());
            //++num
            num.incrementAndGet();
            System.out.println(num.get());
            //num--
            num.getAndDecrement();
            System.out.println(num.get());
            //--num
            num.decrementAndGet();
            System.out.println(num.get());
        });
        t1.start();
    }
}

打印结果如下:

在这里插入图片描述

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

        t1.join();
        t2.join();

        System.out.println(num.get());
    }
}

运行结果如下:最终 num 的值为 20000(固定的)

在这里插入图片描述
原因
getAndIncrement() 方法以原子的方式获得 num 的值,并将 num 进行 ++ 自增操作,即获得值、并将该值增加1,再生成新值,这一整个操作,是原子的,不会被中断,就可以保证在多线程编程环境下,并发地访问同一个实例,计算可以返回正确的值~

我们可以查看源码,发现 getAndIncrement() 方法并没有使用加锁(synchronized)的操作来保证原子性,如下:

1)先点进 getAndIncrement() 方法源码

在这里插入图片描述
2)再点进 getAndAddInt() 方法源码,可以看到,其中使用了 CAS 机制
在这里插入图片描述
3)再点进 compareAndSwapInt() 方法,可以发现,这是一个由 native 修饰的方法,CAS 机制的实现依赖于底层硬件和操作系统提高的原子操作支持,它是更偏向底层的操作~

在这里插入图片描述


上述是线程安全的案例,接着,线程不安全案例,与之形成对比,代码如下:
class Counter {
    private int count = 0;

    //count++操作
    public void add() {
        count++;
    }
    //得到count的值
    public int get() {
        return count;
    }
}

public class ThreadDemo {

        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();

            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.add();
                }
            });

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.add();
                }
            });

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

            t1.join();
            t2.join();

            System.out.println(counter.get());
        }
}

运行的结果,如下:
在这里插入图片描述
原因
在实际中,线程的调度顺序是无序的,不能确定 t1 和 t2 线程在自增过程中是如何执行的,因此,结果是不确定的~ 归根结底是线程无序调度的锅!(具体分析可以回顾往期内容:线程安全)

2.1.2 伪代码实现原子类

伪代码如下:

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 是不是变过!如果没变过,才能自增,如果变过了,则先更新,再自增~

之前线程不安全,很大的云因是一个线程不能及时感知到另一个线程对内存的修改,比如 t2 在自增的时候,先读后自增,此时在自增之前,t1 线程已经自增过了,t2 线程在 0 的基础上自增的,导致无效自增,就会出现问题,使用 CAS 后, t2 会在自增之前,先检查一下寄存器的值和内存的值是否一致!只有一致才会执行自增,否则重新将内存中的值更新,与寄存器同步~

这个操作不涉及阻塞等待,因此,会比加锁解决线程不安全问题的方案快很多

2.2 实现自旋锁

2.2.1 自旋锁是什么

自旋锁
自旋锁是一种忙等待的锁机制,如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁,一般乐观锁的情况下,锁冲突概率低,实现自旋锁比较合适~ 实现自旋,目的就是为了忙等,就是为了能够最快的速度拿到锁!
(可回顾这一期内容:常见的锁策略)

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

图解如下:
在这里插入图片描述

  • owner 记录当前的锁被哪个线程持有,如果为null,则说明没有线程持有,锁处于空闲状态

  • 比较 owner 和 null 是否相等(即判断 owner 是否为 null,锁是否为空闲状态)

    如果是相等,owner 是为 null,则进行交换,将当前线程的引用交换到 owner 中,加锁完成,交换成功则返回 true,循环就结束

    如果是不相等,意味着 owner 不为 null,锁已经有线程持有了,此时 CAS 就什么都不用做,返回 fasle,循环继续

    此时这个循环就会转得飞快,不停地尝试询问这里的锁是不是被释放了,一旦释放,就能立即获取到锁(实现自旋锁),坏处就是造成了 CPU 忙等

  • unlock()方法,解锁操作,即直接把 owner 设置为 null

CAS 机制,解释了自旋锁的实现~

3. CAS 的 ABA 问题

3.1 ABA 问题

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

CAS 的关键是对比内存和寄存器的值,看是否相同,就是通过这个比较来检测内存中的值是否发生改变

ABA 问题】可能存在这样一个情况,对比的时候是相同的,但其值不是没有变过,而是从A值变成B值又变回A值(A->B->A),此时,有一定概率会出问题,CAS 只能对比值是否相同,但不能确定这个值是否中间发生过改变(这就类似于在某鱼上买个手机,结果是个"翻新机"是一样)

图解如下:

在这里插入图片描述

3.2 ABA 问题引起的 BUG

ABA 问题,大部分情况下,都没事,但是,小概率下会出现 BUG!!!

假设这样一场景:取钱(这可是不能出现 BUG 的噢,毕竟对钱都很敏感hh)

小万有 1000 元存款,她要从 ATM 机中取出 500 元钱,此时,取款机创建了两个线程 t1、t2,并发执行 账户 -500 的操作

我们期望的是 —— 这两个线程中,一个线程执行 -500 的操作,而另一个线程 -500 失败

如果是 CAS 的方式来完成这个扣款,就可能出现问题,引发 BUG

  • 正常情况
    1)存款1000,线程 t1 获取到当前存款 1000,期望值更新为 500;线程 t2 获取到当前存款 1000,期望值更新为 500
    2)线程 t1 执行扣款成功,账户减少 500,存款变为 500,线程 t2 阻塞等待
    3)线程 t2 执行,当前存款为 500,与之前读到的 1000,对比,不相同,执行失败
  • 异常情况
    1)存款1000,线程 t1 获取到当前存款 1000,期望值更新为 500;线程 t2 获取到当前存款 1000,期望值更新为 500
    2)线程 t1 执行扣款成功,账户减少 500,存款变为 500,线程 t2 阻塞等待
    3)线程 t2 执行前,小万的朋友小丁,转给小万 500,此时小万的账户余额还是 1000!
    4)线程 t2 执行,当前存款为 1000,与之前读到的 1000,对比,相同,再次执行扣款操作

此时,扣款 500 的操作被执行了两次!这是 ABA 问题引起的 BUG

如何解决 ABA 问题呢?

3.2 ABA 问题的解决方案 —— 使用版本号

ABA 关键是值会反复横跳,如果约定数据只能单方向变化,即数据只能增加,或者只能减少,问题就迎刃而解了

  • Q:如果需求要求该数值,既能增加也能减少,那怎么解决呢?
  • A:可以引入另外一个版本号变量,约定版本号只能增加,每次修改,都会增加一个版本号就能感知值是否发生变化

每次 CAS 对比,就不是对比数值本身,而是对比版本号

只要约定版本号,只能递增,就能保证此时不会出现 ABA 反复横跳问题,以版本号为基准,而不是以变量数值为基准了(即 CAS 在对比的时候,对比的不是数值本身,而是对比版本号,这样其它线程在进行 CAS 操作时,可以检查版本号是否发生变化,从而避免 ABA 问题)

图解如下:

在这里插入图片描述

💛💛💛本期内容回顾💛💛💛

在这里插入图片描述
✨✨✨本期内容到此结束啦~

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

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

相关文章

【2024最新】基于springboot+vue的教师人事档案管理系统lw+ppt

作者&#xff1a;计算机搬砖家 开发技术&#xff1a;SpringBoot、php、Python、小程序、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;Java精选实战项…

wxPython中wx.ListCtrl用法(样式和事件)

wx.ListCtrl是一个列表组件&#xff0c;可以以列表视图&#xff08;list view&#xff09;、报表视图&#xff08;report view&#xff09;、图标视图&#xff08;icon view&#xff09;和小图标视图&#xff08;small icon view&#xff09;等多种模式显示列表。 组件样式 wx…

c++grpc详解

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一.简介支持的语言使用场景特点 二.传输原理protobuf传输HTTP2.0流和帧 三.grpc的四种模式1.一元RPC模式2.服务端流3.客户端流4.双向流 四.基本流程 一.简介 gRPC …

Lesson3 - 操作系统软件视角和系统调用

文章目录 硬件支持系统 系统管理硬件异步行为中断的分类 同步行为虚拟地址空间shell系统调用与软中断区分系统调用trace 命令 硬件支持系统 系统管理硬件 计算机硬件由三样东西组成&#xff1a;CPU、内存、I/O设备。为了更有效地管理这些硬件资源&#xff0c;系统设计者引入了…

ElasticSearch备考 -- Search template

一、题目 ### 基础版 Create a search template for the above query, so that the template (i) is named "with_response_and_tag", (ii) has a parameter "with_min_response" to represent the lower bound of the response field, (iii) has a parame…

分治算法(3)_快速选择_数组中的第K个最大元素

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 分治算法(3)_快速排序_数组中的第K个最大元素 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#…

SSH -L 代理与反向代理转发详解

简介&#xff1a;SSH -L 选项用于设置本地端口转发&#xff0c;而反向代理转发则允许远程主机访问本地服务。本文将介绍如何使用 SSH -L 实现本地端口转发和反向代理转发&#xff0c;并提供示例以帮助您理解和应用这些技术。 历史攻略&#xff1a; Centos&#xff1a;设置代理…

国庆假期结束

&#x1f51a; 推迟几天返校 未提前和老师商量&#xff08;其实放假前我也说过的&#xff0c;但是我导可能忘记了&#xff09; 我的确有错&#xff0c;事情总自己觉得行了就觉得可以了 在老师看起来的确有点“不尊重” 下次一定要要要注意⚠️⚠️⚠️ 上次&#xff0c;国…

【Kubernetes】常见面试题汇总(五十九)

目录 129.问题&#xff1a;pod 使用 PV 后&#xff0c;无法访问其内容&#xff1f; 130.查看节点状态失败&#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#xf…

大语言模型 LLM 量化技术略解

什么是量化? 随着语言模型规模的不断增大,其训练的难度和成本已成为共识。而随着用户数量的增加,模型推理的成本也在不断攀升,甚至可能成为限制模型部署的首要因素。因此,我们需要对模型进行压缩以加速推理过程,而模型量化是其中一种有效的方法。 大语言模型的参数通常…

Python运行态 - 代码调试:掌握pdb

简介&#xff1a;pdb&#xff08;Python Debugger&#xff09;是 Python 标准库中的调试工具&#xff0c;旨在帮助开发者在代码中设置断点、检查变量值和逐行执行代码。这对于定位和修复程序中的问题至关重要。pdb 是 Python 的内置模块&#xff0c;因此不需要额外安装。 历史…

20款奔驰CLS300升级原厂抬头显示HUD 23P智能辅助驾驶 触摸屏人机交互系统

以下是为您生成的一份关于 18 款奔驰 CLS 老款改新款的改装文案&#xff1a; 18 款奔驰 CLS 老款改新款&#xff1a;科技升级&#xff0c;畅享极致驾驶体验 在汽车改装的世界里&#xff0c;每一次的升级都是对卓越的追求。今天&#xff0c;让我们一同探索 18 款奔驰 CLS 老款改…

Leetcode—152. 乘积最大子数组【中等】

2024每日刷题&#xff08;174&#xff09; Leetcode—152. 乘积最大子数组 C实现代码 class Solution { public:int maxProduct(vector<int>& nums) {int n nums.size();int mx nums[0];int mn nums[0];int ans mx;for(int i 1; i < n; i) {const int prem…

贪心算法c++

贪心算法C概述 一、贪心算法的基本概念 贪心算法&#xff08;Greedy Algorithm&#xff09;&#xff0c;又名贪婪法&#xff0c;是一种解决优化问题的常用算法。其基本思想是在问题的每个决策阶段&#xff0c;都选择当前看起来最优的选择&#xff0c;即贪心地做出局部最优的决…

基于yolov8的200鸟类智能检测与识别系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的200种鸟类智能检测与识别系统是一款基于深度学习的目标检测系统&#xff0c;该系统利用YOLOv8框架&#xff0c;通过11788张图片训练出一个能够进行鸟类智能检测与识别的模型&#xff0c;可以识别200种不同的鸟类。 该系统采用Python与PyQt5开发&…

苹果电脑磁盘满了怎么清理内存?必看清理秘籍

对于很多Mac用户来说&#xff0c;随着时间的推移&#xff0c;电脑逐渐变慢并出现磁盘空间不足的提示是一件非常头疼的事情。正确理解内存和存储的区别&#xff0c;并采用有效的清理方法&#xff0c;对于保持Mac性能至关重要。本文将深入探讨如何有效清理Mac上的磁盘空间&#x…

一文看懂计算机中的大小端(Endianess)

文章目录 前言一、什么是大小端二、如何判断大小端三、大小端的转换3.1 使用标准库函数3.2 手动实现大小端转换 前言 本文主要探讨计算机中大小端的相关概念以及如何进行大小端的判断和转换等。 一、什么是大小端 大小端&#xff08;Endianess&#xff09;是指计算机系统在存…

CentOS7安装宝塔

第一步&#xff1a;安装 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh安装成功后会弹出这个页面 那么下次怎样再次打开这个页面呢&#xff1f; 输入这个&#xff1a; /etc/init.d/bt default…

YOLO v1详解解读

&#x1f680; 在此之前主要介绍了YOLO v5源码的安装和使用&#xff08;YOLO v5安装教程&#xff09;&#xff0c;接下来将探索YOLO的实现原理&#xff0c;作为一个金典的单阶段目标检测算法&#xff0c;应该深度的理解它的构建思想&#xff1b;所以本系列文章将从LOVO v1出发到…

JavaEE: 深入解析HTTP协议的奥秘(1)

文章目录 HTTPHTTP 是什么HTTP 协议抓包fiddle 用法 HTTP 请求响应基本格式 HTTP HTTP 是什么 HTTP 全称为"超文本传输协议". HTTP不仅仅能传输文本,还能传输图片,传输音频文件,传输其他的各种数据. 因此它广泛应用在日常开发的各种场景中. HTTP 往往是基于传输层的…