本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
什么是CAS
CAS又叫比较并交换,是一种无锁算法,日常开发中,基本不会直接用到CAS,都是通过一些JDK封装好的并发工具类来使用的,在JUC包下。
CAS包含三个值,内存地址(V),预期值(A),新值(B)。先比较内存地址的值和预期的值是否相等,如果相等,就将新值赋在内存地址上,否则,不做任何处理。步骤如下:
1.获得字段的期望值(oldValue)。
2.计算出需要替换的新值(newValue)。
3.通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败则重复第1步到第2步,一直到CAS成功,这种重复也就是CAS自旋。
当CAS进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多,CAS性能会大大降低。所以,提升CAS无锁编程的效率,关键在于减少冲突的机会。
CAS原理剖析
以AtomicInteger原子整型类为例,看一下CAS底层实现机制。
1 | java复制代码public class AtomicInteger extends Number implements java.io.Serializable { |
1 | arduino复制代码//实际对数据操作的是unsafe的类的getAndAddInt方法 |
AtomicInteger 内部方法都是基于Unsafe类实现的,Unsafe类是个跟底层硬件CPU指令通讯的复制工具类。
再看看unsafe.getAndAddInt方法具体内容:
1 | arduino复制代码//CAS自旋,通过getIntVolatile方法通过内存偏移量获取对象最新的值,再调用cas方法,如果失败了就不断的重 |
再看看如何获得valueOffset的:
1 | csharp复制代码// Unsafe实例 |
value实际的变量,是由volatile关键字修饰的,为了保证在多线程下的内存可见性。
CAS的问题
ABA问题
CAS操作是先比较A的预期值和内存地址中的值是否相同,如果相同就认为此时没有其他线程修改A值。但是,此时假如一个线程读取到A值,此时有另外一个线程将A值改成了B,然后又将B改回了A,这时比较A和预期值是相同的,就认为A值没有被改变过。为了解决ABA的问题,可以使用版本号,每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被别人偷偷改过了。
解决方法:AtomicReference原子引用。
性能问题
如果自旋长时间不成功,会给CPU带来非常大的执行开销。
1 | sql复制代码public final int getAndSet(int newValue) { |
可以看到源码中的自旋就是当CAS成功时,才会return。因此CAS带来的性能问题也是需要考虑的。自旋也是CAS的特点,自旋算是一种非阻塞算法,相对于其他阻塞算法而已,非阻塞是不需要cpu切换时间片保存上下文的,节省了大量性能消耗。CAS相对于同步锁的优点:如果在并发量不是很高时CAS机制会提高效率,但是在竞争激烈并发量大的情况下效率是非常低,因为自旋时间过长,失败次数过多造成重试次数过多。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
ABA问题解决办法
加版本号
每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。
AtomicStampReference的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。
使用AtomicMarkableReference
AtomicMarkableReference不关心修改过几次,仅仅关心是否修改过。其标记属性mark是boolean类型,而不是数字类型,标记属性mark仅记录值是否有过修改。
AtomicMarkableReference适用只要知道对象是否有被修改过,而不适用于对象被反复修改的场景。
CAS的使用场景
CAS在JUC包中的原子类、AQS以及CurrentHashMap等重要并发容器类的实现上都有应用。再看一下AQS的例子:
1 | arduino复制代码protected final boolean compareAndSetState(int expect, int update) { |
对state变量进行的CAS操作,很多同步类都是通过这个变量来实现线程安全的,所以在AQS中,首先要保证对state的赋值是线程安全的。
在java.util.concurrent.atomic包的原子类如AtomicXXX 中,都使用了CAS保障对数字成员进行操作的原子性。
JUC的大多数类(包括显示锁、并发容器)都基于AQS和AtomicXXX实现,而AQS通过CAS保障其内部双向队列头部、尾部操作的原子性。
抽奖说明
1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…
2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!
3.本月的文章都会参与抽奖活动,欢迎大家多多互动!
4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。
本文转载自: 掘金