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