当前位置: 首页 > news >正文

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


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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Beyond Compare解锁版下载及安装 (文件和文件夹比较工具)
  • Adobe Substance 3D Stager v1.2.2.5262 解锁版下载及安装教程( 3D场景设计和渲染软件)
  • 访问网站显示不安全如何处理
  • pinia-plugin-persistedstate 插件不生效
  • WPF Treeview开启虚拟化后如何找到TreeViewItem
  • Docker容器里centos、unbuntu无法使用 systemctl 命令
  • 更换到阿里云的 CentOS 7 镜像源
  • AutoSar AP软件规范中EM介绍及功能概要
  • 新技术环境下保密工作面临的挑战和应对策略
  • 【Python】Python中一些有趣的用法
  • canal监听mysql增量数据发布到rabbitmq
  • JavaWeb—XML_Tomcat10_HTTP
  • PLSQL导入导出ORACLE数据提示失败问题修改PLSQL配置
  • 目标检测研究方向——开放域目标检测
  • 【docker】虚拟化与docker基础
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • 【附node操作实例】redis简明入门系列—字符串类型
  • const let
  • Github访问慢解决办法
  • Iterator 和 for...of 循环
  • Java方法详解
  • JSONP原理
  • mongo索引构建
  • MySQL用户中的%到底包不包括localhost?
  • SegmentFault 2015 Top Rank
  • Zsh 开发指南(第十四篇 文件读写)
  • 缓存与缓冲
  • 回流、重绘及其优化
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • 阿里云重庆大学大数据训练营落地分享
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • ​​​​​​​GitLab 之 GitLab-Runner 安装,配置与问题汇总
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • # windows 安装 mysql 显示 no packages found 解决方法
  • #define,static,const,三种常量的区别
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (ros//EnvironmentVariables)ros环境变量
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (免费领源码)Python#MySQL图书馆管理系统071718-计算机毕业设计项目选题推荐
  • (一) 初入MySQL 【认识和部署】
  • ... 是什么 ?... 有什么用处?
  • .chm格式文件如何阅读
  • .NET C# 使用 iText 生成PDF
  • .net core控制台应用程序初识
  • .Net Redis的秒杀Dome和异步执行
  • .Net程序帮助文档制作
  • .NET开源项目介绍及资源推荐:数据持久层
  • /boot 内存空间不够
  • @ 代码随想录算法训练营第8周(C语言)|Day53(动态规划)
  • @DateTimeFormat 和 @JsonFormat 注解详解
  • [ C++ ] STL---string类的使用指南
  • [2009][note]构成理想导体超材料的有源THz欺骗表面等离子激元开关——