【JavaEE】CAS原理

目录

​前言

什么是CAS?

 如何使用CAS?

CAS实现自旋锁

CAS的ABA问题

面试题

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

2.ABA问题怎么解决?


前言

在多线程中,多个线程同时对一个共享变量进行读写操作,那么就会出现线程安全问题,那就得使用synchronized进行加锁,但我们加锁,可能会出现死锁等问题。所以今天我们就来认识一种不需要加锁,也能实现原子性的算法--CAS

什么是CAS?

CAS (Compare and Swap) 是一种无锁算法中的原子操作,在多线程环境下用于实现数据的原子更新。CAS包含了三个操作数:内存值V旧的预期值A、和需要修改的新值B。当进行CAS操作时,会将内存值V和旧的预期值A进行比较,如果V和A相等,那就把内存值V更新为新值B;如果不相等,那么就不进行任何操作。

在多线程并发编程中,如果线程之间的锁冲突较低的情况下,那么就可以使用CAS,性能会优于synchronized,避免了线程上下文切换的开销

CAS是由CPU上的一条指令完成的,具有原子性。

我们来看下CAS伪代码,方便理解:

/*** 比较并交换地址处的值* 此方法用于原子地比较地址处的值是否与期望值相等,如果相等,则将地址处的值替换为新值* 这是一个基本的同步原语操作,常用于实现线程安全的算法和数据结构* * @param address 要访问的内存地址* @param expectValue 期望值,即认为当前地址处应该具有的值* @param swapValue 如果地址处的值等于期望值,则用此值替换地址处的值* @return 如果地址处的值等于期望值,则返回true,否则返回false*/boolean CAS(address, expectValue, swapValue) {// 检查地址处的当前值是否与期望值相等if (&address == expectedValue){// 如果相等,则将地址处的值替换为新值,并返回操作成功&address = swapValue;return true;}// 如果地址处的值与期望值不相等,则返回操作失败return false;}

CAS其实是CPU中的指令,但被操作系统、JVM 等层层封装后提供给上层使用。在java中,CAS操作主要是通过java.util.concurrent.atomic包中的原子类来实现的,如 AtomicIntegerAtomicLong等。 

我们来看AtomicInteger的中getAndIncrement(相当于前置++)方法的伪代码:

    /*** 原子整型类,提供原子操作的方法* 用于在多线程环境下安全地操作整型数值,防止数据不一致的情况*/class AtomicInteger {// 原子整型变量的当前值private int value;/*** 获取当前值并原子性地进行自增* * 此方法保证在多线程环境下安全地读取和更新整型变量的值* 使用CAS操作来实现原子性,直到CAS成功为止* * @return 当前值(自增前的值)*/public int getAndIncrement() {// 读取当前值,准备进行CAS操作int oldValue = value;// 使用CAS操作尝试将value值从oldValue递增为oldValue+1,直到成功为止while (CAS(value, oldValue, oldValue + 1) != true) {// CAS操作失败时,重新读取value值,确保数据一致性oldValue = value;}// 返回自增前的值return oldValue;}}

假设现在有线程t1和t2,要对某个变量进行++操作(分为读写存三个指令),那么没有使用CAS之前,当t1刚读取完值,此时切换到线程t2,当t2执行完指令,此时值为1,当由于此时t1线程原先读取到的旧值为0,那么此时t1线程继续执行指令,就会覆盖到t2的值,此时就会出现线程安全问题。

这里我们加上CAS操作之后,在t1线程读取完值后,此时切换到线程t2,t2在读取完内存值和期望的旧值之后,判断相等,将value更新为1。线程2执行完操作,切换到t1线程,此时t1线程判断内存值=1和期望的旧值=0,发现不相等,就不会进行交换操作,重新进行一次load操作。此时vaue为1,且oldvalue也为1,进行更新操作,value=2。

 如何使用CAS?

在java中,给我们提供了java.util.concurrent.atomic供我们使用。我们可以在java8查看一下atomic包中相关的方法。

Overview (Java Platform SE 8 ) (oracle.com)

如果你想要对int类型的数据进行操作,那么就可以创建一个AtomicInteger类,若想对Boolean类型的数据进行操作,那就创建一个AtomicBoolean的原子类。 

这里我们拿AtomicInteger为例

示例:假设现在有两个线程t1和t2,我们想使用使用者两个线程对count进行++操作,最终值为10000。

如果我们不进行任何加锁操作,那么就会有线程安全问题。

class Demo{private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<5000;i++){count++;}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

那么我们就会得到一个小于10000的值

所以我们可以用AtomicInteger。由于是个原子类,所以我们需要创建一个原子类对象。

这里我们可以指定初始值或者默认值为0。

 我们想要进行++操作,这里由于count是个类对象,不能直接++,需要调用其中的方法,这里前置++对应的方法是incrementAndGet(),后置++则是getAndIncrement()

class Demo{public static void main(String[] args) throws InterruptedException {AtomicInteger count=new AtomicInteger(0);Thread t1=new Thread(()->{for(int i=0;i<5000;i++){count.getAndIncrement();}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}

相同的,如果我们想要将count=10000减为0,那么我们可以使用getAndDecrement,

class Demo{public static void main(String[] args) throws InterruptedException {AtomicInteger count=new AtomicInteger(10000);Thread t1=new Thread(()->{for(int i=0;i<5000;i++){count.getAndDecrement();//count--}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){count.getAndDecrement();//count--}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}

当然,在AtomicInteger中也有很多其他的方法,以下是常用的方法:

这里就不做一一介绍,想了解其中其他方法,可以点击Java 平台 SE 8 (oracle.com) 查看。

 我们知道,++和--是不具有原子性,那么这里getAndIncrement() 和getAndDecrement为什么具有原子性?

我们可以ctrl+鼠标左键进入getAndIncrement方法内部

我们可以看到在 getAndIncrement内部又调用了getAndAddInt方法,而这个方法是属于U,这U就是unsafe类,这个类下的方法都是偏底层且危险的操作。

 

我们接着点击weakCompareAndSetInt方法。

点击compareAndSetInt方法查看。 

 可以看到compareAndSwapInt 方法是 native 修饰的本地方法,这个方法是 JVM 底层由 C/C++ 写的,我们是看不到的。

伪代码演示getAndIncrement

   /*** 原子整型类,提供原子操作的方法* 用于在多线程环境下安全地操作整型数值,防止数据不一致的情况*/class AtomicInteger {// 原子整型变量的当前值private int value;/*** 获取当前值并原子性地进行自增* * 此方法保证在多线程环境下安全地读取和更新整型变量的值* 使用CAS操作来实现原子性,直到CAS成功为止* * @return 当前值(自增前的值)*/public int getAndIncrement() {// 读取当前值,准备进行CAS操作int oldValue = value;// 使用CAS操作尝试将value值从oldValue递增为oldValue+1,直到成功为止while (CAS(value, oldValue, oldValue + 1) != true) {// CAS操作失败时,重新读取value值,确保数据一致性oldValue = value;}// 返回自增前的值return oldValue;}}

CAS实现自旋锁

在前面锁策略中,我们已经了解了什么是自旋锁,自旋锁就是当线程去获取锁时,此时锁被其他线程所持有,此时线程不会进入线程等待状态,而是会占用CPU资源,反复判断这个锁是否被释放,直到拿到锁为止。整个操作处于用户态,减少内核态的一些操作。

接下来,我们来使用CAS来实现自旋锁。

public class SpinLock{//用来记录锁被哪个线程持有,当owner为null时,说明没有线程持有锁,可被获取private Thread owner=null;//while循环来判断当前线程是否获取到锁,当owner为null时,说明没有线程持有锁,可被获取//反之,则说明此时锁被其他线程占用,无法获取到,返回false,取反之后就为true,进入死循环//直到获取到锁。当获取到锁后,owner被设置为当前线程,返回true,取反后就为false,退出循环public void Lock(){while(!cas(owner,null,Thread.currentThread())){}}/*** 解锁当前对象的锁状态* 通过将owner属性设置为null,表示当前对象不再被任何线程锁定*/public void unLock(){this.owner=null;}
}

CAS实现的自旋锁适用于那些临界区代码执行时间非常短、并且锁的竞争不是很激烈的场景。

CAS的ABA问题

cas是通过判断内存中的数据和寄存器中的数据是否相等来更新值的,但有一种潜在的情况:这个内存中的数据由A->B->A,也就是内存数据被修改了一次,最后又修改为原来的数据。这种情况会出现问题吗?

我们来举个例子:假设我有5000块钱,我想要给我的好兄弟通过支付宝转500块钱,当我找到我好兄弟的账号后点击转账500元,但此时由于网络延迟问题,我又点击了一次,当网络好转之后,显示转账成功,但是在后台可能提示我转账两次,实际我只想转一次,但点击两次那么支付宝的后台就会有两个线程在执行cas操作。

对于上面这种情况,一般是不会出现问题的。

如果我们在转账成功之前,由其他人给我转进500块钱,会发生什么?

 可以看到,当我给我的好兄弟转了500之后,此时因为有其他人在此时又给我转了500,那在t1线程中,就判断我的钱还没有转,就又一次给我的好兄弟转了500,这样一共就给我的好兄弟转了1000块钱了。

那么如何防止这种情况发生呢? 

造成ABA问题就是因为变量能加也能减,如果只能加不能减或者只能减不能加,就不会有这种情况,因此,我们引入一个变量:版本号,如果我们对余额修改一次,那么版本号就+1,当cas在执行的时候,通过判断当前版本号和预期的旧的版本号是否相等,若相等,则说明还没有进行转账,若不相等,说明中间穿插了其他的修改操作,不进行修改操作。

/*** Test类用于演示并发下的资金转账操作* 通过CAS操作保证原子性,防止数据一致性问题*/
class Test{// 账户余额,初始值为5000private int value=5000;// 版本号,用于CAS操作判断数据是否被修改private int version=0;/*** 转账方法,将指定金额添加到账户余额中* @param money 要转账的金额*/public void transfer(int money){// 读取当前版本号int oldVersion=version;// 尝试更新版本号,确保操作的原子性if(CAS(version,oldVersion,oldVersion+1)) {// 更新成功后,增加账户余额value+=money;}}
}

面试题

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

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

2.ABA问题怎么解决?

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


以上就是本篇所有内容,若有不足,欢迎指正~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

01 NoSQL之Redis配置与优化

目录 1.1 Redis介绍 1.1.1关系数据库与非关系型数据库 1 . 关系型数据库 2. 非关系型数据库 3.非关系型数据库产生背景 (1) High performance--对数据库高并发读写需求 &#xff08;2) Huge Storage--对海量数据高效存储与访问需求 &#xff08;3) High Scalability …

gitlab cicd快速入门有哪些方法 gitlabcicd和Jenkins哪个更好用

在现代软件开发中&#xff0c;持续集成和持续交付&#xff08;CI/CD&#xff09;已成为必不可少的流程。它们不仅能提高开发效率&#xff0c;还能保证代码的质量和稳定性。在众多CI/CD工具中&#xff0c;GitLab和Jenkins是最为常用的两种。本文将围绕“gitlab ci/cd快速入门有哪…

vuex properties of undefined (reading ‘getters‘)

前言&#xff1a; 最近打算用vue 写个音乐播放器&#xff0c;在搞 vuex 的时候遇到一个很神奇报错&#xff1b;vuex 姿势练了千百次了&#xff0c;刚开始的时候我一直以为是代码问题&#xff0c;反复检查了带了&#xff0c;依旧报错。 Error in mounted hook: "TypeError:…

[Android] [解决]Bottom Navigation Views Activity工程带来的fragment顶部空白间距问题

用Android Stuio创建一个Bottom Navigation Views Activity工程&#xff0c; 我们刻意设置一下fragment背景为黑色&#xff0c;会发现&#xff0c;这个fragment离顶部还有一段不小空白距离&#xff0c; 怎么解决呢&#xff1f; 在activity_main.xml里面&#xff0c;删掉这句&a…

极狐GitLab安全版本:16.10.1、16.9.3、16.8.5

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门面向中国程序员和企业提供企业级一体化 DevOps 平台&#xff0c;用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规&#xff0c;而且所有的操作都是在一个平台上进行&#xff0c;省事省心省钱。可以一键安装极狐GitL…

数据结构之线性表(单链表的实现)

目录 一、单链表的原理 二、单链表的实现 1.单链表的定义 2.单链表的初始化 3.清空单链表 4.单链表是否为空 5.单链表的长度 6.获取指定位置 i 的元素 7.获取指定元素 e 的位置 8.向链表中插入指定位置的元素 9.向链表中删除指定位置的元素 10.遍历链表中的元素 …

告别手动操作!KeyMouseGo实现自动化工作流

前言 在这个快节奏的时代&#xff0c;我们每天都在与电脑打交道&#xff0c;重复着那些繁琐而单调的操作&#xff1b;你是否曾想过&#xff0c;能让电脑自己完成这些任务&#xff0c;而你则悠闲地喝着咖啡&#xff0c;享受着生活&#xff1f;今天&#xff0c;就让我们一起揭开一…

【sdk】- 对接阿里云抠图

文档地址&#xff1a;https://help.aliyun.com/zh/viapi/use-cases/general-image-segmentation?spma2c4g.11186623.0.0.3814173cenldIs java对接阿里云的通用分割&#xff0c;将代码原封不动复制进来&#xff0c;执行结果失败&#xff0c;咨询阿里云的人员之后&#xff0c;由…

优盘数据救援:应对文件与目录损坏的全方位指南

一、问题剖析&#xff1a;优盘文件或目录损坏的困境 在数字化时代&#xff0c;优盘&#xff08;USB闪存驱动器&#xff09;作为便携、高效的数据存储工具&#xff0c;广泛应用于数据传输、备份与分享。然而&#xff0c;面对频繁的使用与不当操作&#xff0c;优盘中的文件或目录…

FPGA常见型号

FPGA&#xff08;现场可编程门阵列&#xff09;开发板种类繁多&#xff0c;涵盖了从入门级教育用途到高性能工业应用的广泛领域。以下是一些常见的 FPGA 开发板型号及其特点&#xff1a; 1. Xilinx&#xff08;赛灵思&#xff09;系列 Xilinx 是 FPGA 领域的领导者之一&#…

Ubuntu22.04安装Docker教程

简介 ​ Docker 是一个开源的平台&#xff0c;旨在简化应用开发、交付和运行的过程。通过使用容器技术&#xff0c;Docker 能够让开发人员将应用及其依赖环境一同打包&#xff0c;从而实现快速部署、一致的开发环境和优秀的可移植性。 系统版本 ​ 本文以Ubuntu 22.04.4 LTS…

【探索Linux】P.46(高级IO —— 五种IO模型简介 | IO重要概念)

阅读导航 引言一、五种IO模型1. 阻塞IO&#xff08;1&#xff09;定义&#xff08;2&#xff09;特点 2. 非阻塞IO&#xff08;1&#xff09;定义&#xff08;2&#xff09;特点 3. IO多路复用&#xff08;1&#xff09;定义&#xff08;2&#xff09;特点 4. 信号驱动IO&#…

深入探索:【人工智能】、【机器学习】与【深度学习】的全景视觉之旅

目录 第一部分&#xff1a;人工智能、机器学习与深度学习概述 1.1 人工智能的概念与发展 代码示例&#xff1a;简单的AI决策系统 1.2 机器学习的定义与分类 代码示例&#xff1a;简单的线性回归模型 1.3 深度学习的基础与应用 代码示例&#xff1a;构建简单的神经网络 …

【TwinCAT】TwinCAT3 PLC HMI在WIN10系统中的全屏显示及用户管理

在工业自动化领域,TwinCAT3 PLC HMI 是一款强大的可视化工具,它支持多种操作系统,并且能够满足不同控制器的需求。在本文中,我们将详细介绍如何在WIN10系统中进行全屏显示设置以及如何进行用户管理配置。 一、TwinCAT3 PLC HMI 全屏显示方法 1. 创建Visualization和配置Vi…

Git开发流程

Git开发流程规范如下&#xff1a; 详情参考&#xff1a;https://www.processon.com/view/link/5d0cf86ce4b0376de9c19e16

【自动驾驶】ROS中自定义格式的服务通信,含命令行动态传参(c++)

目录 通信流程创建服务器端及客户端新建服务通讯文件修改service的xml及cmakelistCMakeLists.txt编辑 msg 相关配置编译消息相关头文件在cmakelist中包含头文件的路径在service包下编写service.cpp在client包下编写client.cpp测试运行查询服务的相关指令列出目前的所有服务&…

【Vue3】组件通信之mitt

【Vue3】组件通信之mitt 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的日…

什么是智能加密?超好用神器智能加密软件推荐

信息安全已成为我们日常生活中不可忽视的一环。 随着网络攻击手段的不断升级和隐私泄露事件的频发&#xff0c;如何有效保护个人及企业的敏感数据成为了亟待解决的问题。 智能加密&#xff0c;作为信息安全领域的一项重要技术&#xff0c;正逐渐走进大众视野&#xff0c;以其高…

【数据结构七夕专属版】单链表及单链表的实现【附源码和源码讲解】

本篇是博主在学习数据结构时的心得&#xff0c;希望能够帮助到大家&#xff0c;也许有些许遗漏&#xff0c;但博主已经尽了最大努力打破信息差&#xff0c;如果有遗漏还请见谅&#xff0c;嘻嘻&#xff0c;前路漫漫&#xff0c;我们一起前进&#xff01;&#xff01;&#xff0…

超声波眼镜清洗机哪家强?盘点四款精品超声波清洗机机型

超声波清洗机因其卓越的清洁能力&#xff0c;成为了家庭和专业环境中清洁小物件的理想选择。无论是日常佩戴的眼镜、珍贵的珠宝首饰&#xff0c;还是精密的电子设备和实验工具&#xff0c;超声波清洗机都能提供深层、温和且高效的清洁。然而&#xff0c;面对市场上众多品牌和价…