CAS详解.

news2024/9/29 11:41:24

CAS这个机制就给实现线程安全版本的代码,提供了一个新的思路,之前通过加锁,把多个指令打包成整体,来实现线程安全。现在就可以考虑直接基与CAS来实现一些修改操作,也能保证线程安全(不需要加锁)

1.什么是 CAS

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

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1. 比较 A 与 V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V。(交换)
3. 返回操作是否成功。

CAS 伪代码

下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解
CAS 的工作流程.

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

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

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

2.CAS 是怎么实现

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

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性

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

3.CAS 有哪些应用

(1)实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
 

package thread2;

import java.util.concurrent.atomic.AtomicInteger;

public class Test7 {
    private static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

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

    }
    
}

 

不会出现线程安全问题!

伪代码实现getAndIncrement:

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 是一个局部变量, 在栈上. 每个线程有自己的栈)

 

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

  • CAS 是直接读写内存的, 而不是操作寄存器.
  • CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的.
     

(3) 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环.在循环里重新读取 value 的值赋给 oldValue


 

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


 

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

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

本来 check and set 这样的操作在代码角度不是原子的. 但是在硬件层面上可以让一条指令完成这
个操作, 也就变成原子的了.

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

通过CAS,比较一下, owner是不是null (问问这个锁,是不是空闲的,没人用的)
如果是空闲的,就直接把当前线程的值,赋值给这里owner就相当于表示了当前的锁被这个线程给获取到了~~
如果当前锁不是空闲的,此处CAS的比较就会失败,直接返回false,循环继续再进行一次再重复上述的CAS过程~
直到owner为 null,这个循环CAS操作比较交换成功,才能从lock方法中返回~~
自旋锁的特点,就是在其他线程释放了锁的瞬间,就能感知到,并且获取到锁~~

4.CAS 的 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, 还是经历了一个变化过程.

ABA 问题引来的 BUG

大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一些特殊情况

假设 滑稽老哥 有 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 问题搞的鬼

解决方案

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

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

真正修改的时候,

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 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, 版本小于当前版本, 认为操作失败.

都版本不一样了咋办?线程一是version=2,线程2是version=1,都读取失败,重新从内存读取至版本和值。 

在 Java 标准库中提供了 AtomicStampedReference<E> 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能.
 

1) 讲解下你自己理解的 CAS 机制

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

2) ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当
前版本号比之前读到的版本号大, 就认为操作失败


 

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

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

相关文章

OpenAi-chatgpt注册保姆级全网最详细注册教程2023年2月最新-

废话就不多说了&#xff0c;说多了浪费各位师傅的时间&#xff01;直接冲&#xff0c;在开始之前需要科学上网&#xff0c;就没其他要求了 1、访问https://chat.openai.com/auth/login 2、点击sign up,输入账号密码&#xff0c;点击Continue 3、之后会来到登陆页面&#xff0…

Oracle Dataguard(主库为 Oracle rac 集群)配置教程(03)—— 创建 dataguard 数据库之前的准备工作

Oracle Dataguard&#xff08;主库为 Oracle rac 集群&#xff09;配置教程&#xff08;03&#xff09;—— 创建 dataguard 数据库之前的准备工作 / 本专栏详细讲解 Oracle Dataguard&#xff08;Oracle 版本为11g&#xff0c;主库为双节点 Oracle rac 集群&#xff09;的配置…

云计算|OpenStack|错误记录和解决方案(不定时更新)

前言&#xff1a; openstack的部署和使用是难度比较大的&#xff0c;难免会出现各种各样的问题&#xff0c;因此&#xff0c;本文将把一些在部署和使用openstack社区版时出现的错误做一个记录&#xff0c;并就每一个错误分析和解决问题。&#xff08;尽量记录比较经典的错误&a…

微搭低代码从入门到精通10-tab栏组件

在小程序中&#xff0c;如果你的页面是由多个组成的&#xff0c;往往涉及到页面切换的问题。那如何引导用户访问不同的页面呢&#xff1f;微搭中提供了tab栏组件来实现这个功能&#xff0c;本篇我们介绍一下这个组件的使用方法。 首先呢打开我们的应用编辑器&#xff0c;在左侧…

OJ刷题Day2 · 判断根结点是否等于子结点之和 · 删除有序数组中的重复项 · 合并两个有序链表 · 数组中的第K个最大元素(中等题)

一、判断根结点是否等于子结点之和二、删除有序数组中的重复项三、合并两个有序链表四、数组中的第K个最大元素&#xff08;中等题&#xff09;一、判断根结点是否等于子结点之和 给你一个 二叉树 的根结点 root&#xff0c;该二叉树由恰好 3 个结点组成&#xff1a;根结点、左…

SpringBoot + kotlin/java + Mybatis-Plus +Sqlite + Gradle多模块项目

前言 我自己的业务项目&#xff0c;先用kotlinspringboot 搭建&#xff0c; 发现gradle支持kts脚本&#xff0c;于是我就搭建试试。我就选用了最流行的Sqlite内嵌数据库,虽然H2也不错&#xff0c;但是Sqlite才是最流行的。orm框架我还是选择了Mybatis-Plus &#xff0c;为此中…

Spring Boot的创建和使用

目录 一、Spring Boot介绍 1.1 Spring Boot 是什么 1.2 Spring Boot的优点 二、Spring Boot 项目的创建 2.1 使用idea创建 2.1.1 安装Spring Boot Helper插件 2.1.2 创建 Spring Boot 项目 2.1.3 验证项目是否创建成功 2.2 使用网页创建 三、输出 hello world 一、S…

前端如何提升To B产品用户体验

云计算产品发展的早期常以技术为核心吸引客户&#xff0c;功能的实现是这一时期产品优先考虑的因素。经过数十年的发展&#xff0c;云计算行业已经进入了深耕细作的时代&#xff0c;市场的激烈竞争与云产品快速发展的同时&#xff0c;用户对产品的可用性与易用性也有了更高的要…

交换机中的冗余链路管理

一 交换机冗余链路许多交换机或交换机设备组成的网络环境中&#xff0c;通常使用一些备份连接&#xff0c;以提高网络的健全性&#xff0c;稳定性。备份连接也叫备份链路&#xff0c;冗余链路等。为了解决共享式局域网的碰撞问题&#xff0c;采用了交换机构成的交换式局域网&am…

C语言静态库、动态库的封装和注意事项

1、动态库、静态库介绍 参考博客&#xff1a;《静态库和动态库介绍以及Makefile》&#xff1b; 2、代码目录结构和编译脚本 参考博客&#xff1a;《实际工作开发中C语言工程的目录结构分析》&#xff1b; 3、编写库的流程 (1)明确需求:需求是否合理、需求的使用场景、需求可能遇…

chatgpt:人工智能的一次突破,如何正确的创建用户及使用

Chatgpt的正确创建及使用 chatgpt最近在国内也开始有声音了&#xff0c;其实早在去年12月初&#xff0c;该网站就已经可以在国外进行使用&#xff0c;而且很快渗透到了国外各行各业各个年龄段 &#xff0c;最火的当属国外很多学生用它来生成论文&#xff0c;关键是语句通顺&am…

如何开启多个独立Chrome浏览器

一、简介 作为测试或者开发人员&#xff0c;有些情况下会用到 Chrome 浏览器&#xff0c;但有时是同一个 Chrome 浏览器无法为我们提供隔离开的不同环境。这样 我们就需要清理 cache 、切换账号等&#xff0c;降低了我们的工作效率。今天的主题是如何开启多个独立的 Chrome 浏…

LayUI模板引擎渲染数据

前端模板引擎介绍 接上节Spring boot项目开发实战——&#xff08;LayUI实现前后端数据交换与定义方法渲染数据&#xff09; 模板引擎能简化开发&#xff0c;极大提高效率&#xff0c;小编之前使用过JSP和Thymeleaf&#xff0c;以及python的jinja2&#xff0c;这些是后端的模…

spring(二)-----------如何注入bean

我们从第三方框架mybatis为引&#xff0c;看看如何往spring中注入一个bean 1、纯mybatis开发生成一个mapper对象 如果不使用spring的情况下&#xff0c;mybatis想生成一个mapper对象大概需要做下面的操作&#xff1a; 假设我们有了一个TMapper接口&#xff0c;此时获取该map…

12款开源数据资产(元数据)管理平台选型分析(三)

如上&#xff0c;是ChatGPT的百度指数和微信指数&#xff0c;继2022年12月上旬技术圈火热之后&#xff0c;因为微软、谷歌等巨头的推广加持&#xff0c;ChatGPT成为全球大众热源的话题。各大媒体都在消费这波舆论红利&#xff0c;打开微信公众号&#xff0c;劈天盖地各种姿势的…

前后端学习

最近和锴哥想搞一下前后端接口的事儿&#xff0c;但是不会&#xff0c;所以打算再学学前后端的基础知识&#xff0c;之后好抄作业&#xff0c;做缝纫机&#xff1b;达哥觉得我浮躁&#xff0c;这次一定要支棱起来&#xff1b;这次开始&#xff0c;面向openai学习。 前后端学习1…

ChatGPT (可能)是怎么炼成的

学习自李宏毅老师的课https://www.youtube.com/watch?ve0aKI2GGZNg 1.学习文字接龙 学习方式 GPT只需要在网上阅读大量的句子&#xff0c;不需要人工标注即可学习到大量句子接龙的知识 然而实际上&#xff0c;“你好”后面可以接的字有很多。实际上&#xff0c;GPT学的就是…

3、Go基础数据类型

目录一、Go数据类型二、字符串三、强制类型转换一、Go数据类型 基础数据类型 类型长度(字节)默认值说明bool1falsebyte10uint8&#xff0c;取值范围[0,255]rune40Unicode Code Point, int32int, uint4或8032 或 64 位&#xff0c;取决于操作系统int8, uint810-128 ~ 127, 0 ~…

Freemarker介绍

2. Freemarker介绍 FreeMarker 是一个用 Java 语言编写的模板引擎&#xff0c;它基于模板来生成文本输出。FreeMarker与 Web 容器无关&#xff0c;即在 Web 运行时&#xff0c;它并不知道 Servlet 或 HTTP。它不仅可以用作表现层的实现技术&#xff0c;而且还可以用于生成 XML…

Python数据结构:概念、栈

1.概念 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说&#xff0c;数据结构就是设计数据以何种方式组织并存储在计算机中。 比如:列表、集合与字典等都是一种数据结构。 N.Wirth:“程序数据结构算法’ 2.分类 数据结…