浅谈CAS,一篇就够了

导读:本篇文章讲解 浅谈CAS,一篇就够了,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

浅谈CAS,一篇就够了
wshanshi:喵桑说,我总结完CAS就带我去吃羊蝎子火锅…干饭那必须整起啊…

一、什么是CAS?

CAS:Compare and Swap。从字面意义上来说,就是先进行比较,然后替换。

它是乐观锁思想的一种实现,尤其是在并发量大的业务场景下保证单个实例的原子性,使用较为频繁。java类库中java.util.concurrent.atomic包下一些方法,也均使用CAS处理。

二、悲观锁与乐观锁

CAS是乐观锁思想的一种体现,那乐观锁和悲观锁有什么区别呢?

2.1、悲观锁

悲观锁常见使用是synchronized修饰的代码块或者方法。

在操作数据之前加锁,直到数据操作完成,锁被释放之后,其它线程才可以操作该数据。比如,mysql数据库锁就是悲观锁。

2.2、乐观锁

数据操作不加锁,每次提交之前获取最新值与原获取值进行对比,数据未变更时操作,否则自旋。

2.3、区别

  • 乐观锁是并行的,悲观锁是串行的。

乐观锁:
在这里插入图片描述
悲观锁:
在这里插入图片描述

  • 乐观锁实质并未“加锁”,悲观锁是加了锁的(synchronized)。

三、CAS原理

Compare and Swap,比较并替换。说白了就是:在操作提交之前,与原获取到的值先进行比较,判断这个值有没有被修改。如果未被修改,操作修改。如果已被修改,则重新获取值,提交之前再比较…

举个栗子:关于《损友经常在群里偷偷改我的头衔》这件事。

线程a获取到我的信息,并操作将我的头衔由“三婶”改为了“王胖虎”。
在这里插入图片描述
之后线程b获取到我的头衔为“王胖虎”,又修改了“王胖虎”为“三婶”。
在这里插入图片描述
这是正常的一种非并发流程的体现,我们再来看下面一种情况:

假设线程a和线程b均获取到我的名字“三婶”。且线程a操作修改了“三婶”为“王胖虎”。
在这里插入图片描述

若a线程成功操作之后,线程b在修改提交前获取名称,发现实际读到的头衔是“王胖虎”。
在这里插入图片描述
对比预期值(三婶),发现”三婶”!=“王胖虎”(被修改了)。这时按照CAS原理,就会再次获取预期值(此时预期值为:王胖虎),且提交前获取内存值,进行对比…判断是否一致…

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,需要替换的新值B。

计算规则是:当需要更新一个变量的值的时候,仅当变量的预期值A(原获取)和内存地址V(提交前获取)中实际值相同的时候,才会把内存地址V对应的值替换成B。

如下图示例:

在这里插入图片描述

四、核心Unsafe类库

该类可直接操作内存,所以效率高。且Unsafe类和常量均使用final修饰,单例模式实现,不可继承。通过下图所示静态方法getUnsafe进行实例化,实例化在static块中操作的。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Unsafe可以设置读写某个属性,如下图所示。

在这里插入图片描述

volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新的值。

在这里插入图片描述

五、CAS优缺点

优点:在并发问题不严重的时候,性能方面比synchronized要快。

缺点:不能确保代码块的原子性。因为CAS机制确保的是一个变量的原子性操作,并不能保证整个代码块的原子性。如果多个变量共同进行原子性的更新操作,就需要用lock或者synchronized了。

5.1、自旋

假设线程a和线程b均获取到我的名字“三婶”。线程a操作修改了“三婶”为“王胖虎”,之后线程b在提交前获取名称,发现读到的是“王胖虎”。对比不一致,就会再次获取。

假如这个时候有个线程c把“王胖虎”改为了“胖虎”,线程b读取时又不一致了。这个时候就会一直获取,一直对比…

这种现象称为“自旋”。
请添加图片描述

5.2、ABA情况

还拿上述的栗子来说:

假设线程a和线程b均获取到我的名字“三婶”。然后线程a操作修改了“三婶”为“王胖虎”。

如果这个时候有个线程c成功操作:将“王胖虎”改为了“三婶”。线程b在提交前获取名称,发现读到的是“三婶”,它会以为“这个值并没有发生变化”。

但实质上,这个值可能是多次被修改后,恰巧变为了原始值的一种情况,也就是所谓的ABA.

在这里插入图片描述

六、如何避免ABA情况?

6.1、加版本号

每次操作compareAndSwap后给数据的版本号加1,再次compareAndSwap的时候不仅比较数据,也比较版本号,值相同,若是版本号不同,就不执行成功。

java.util.concurrent.atomic包中提供了AtomicStampedReference来解决该问题。

在这里插入图片描述
AtomicStampedReference 内部维护了一个 Pair的数据结构:reference(数据体)、stamp(版本)两个部分。该数据结构用volatile修饰,保证了线程可见性。
在这里插入图片描述

核心方法为:compareAndSet方法。该方法中,expectedReference:表示预期值,newReference:表示新的值,expectedStamp:表示预期版本号,newStamp表示新的版本号。

在这里插入图片描述
从数据和版本号两个方面来判断传入的参数是否符合 Pair 的预期,有一个不符合就返回false。
在这里插入图片描述

可以看到,这里底层也是使用了cas。预期值为“三婶”,版本号为0。新值为“王胖虎”,新版本号为1。

在这里插入图片描述

而casPair实质上调用的是UNSAFE.compareAndSwapObject()方法。

在这里插入图片描述

由此可见,AtomicStampedReference是通过加版本号来解决ABA问题的。对于加版本号,compareAndSwapObject只能对比交互一个对象,所以将数据和版本号放到一个对象里就可以解决问题了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/115750.html

(0)
Java光头强的头像Java光头强

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!