目录

CAS简介

CAS思路

CAS使用场景

CAS问题

总结


 

CAS简介

CAS的英文全称是Compare-And-Swap,意思就是比较并交换,他是原子类的底层原理,同时也是乐观锁的原理,CAS的特点是避免使用互斥锁,当多个线程同时更新同一个变量时,只有一个线程可以更新成功,其他的线程都会更新失败,和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而是被告知此次竞争失败,下次还可以继续竞争。

CAS思路

首先,在大多数处理器指令中,CAS操作是一条(而不是多条)cpu指令,正是这个原因,所以CAS相关的指令是具备原子性的,这个组合操作在执行期间不会被打断,这样就保证了并发安全。

CAS有三个操作值:内存值V,预期值A,要修改的值B,当且仅当预期值A等于当前内存值V的时候,才将内存值更新为B。

CAS操作描述

执行成功

当执行CAS操作的时候,如果发现当前的内存值V正好是值A的话,那么CAS就会更新内存值V为B,通常A往往是之前读取到的内存值, B是在A的基础上计算而得到的。例图如下:

 -LMLPHP

执行失败

dangzhixingCAS操作的时候,如果发现当前的内存值V不是预期值A,那么说明内存值已经被其他线程执行过了,本次CAS就会失败,这样就可以避免多人同时修改出错。例图如下:

 -LMLPHP

CAS使用场景

数据库

基于数据库我们可以实现利用version字段在数据库中实现乐观锁和CAS操作,在获取和修改数据都不需要加悲观锁,例如:

UPDATE TABLE  SET name ='测试', version = 2  WHERE id=1 and vsersion =1

原子类

很多原子类中都用了CAS操作,例如AtomicInteger的getAndAdd(int data)方法,如下图:getAndAdd是AtomicInteger类中的方法,getAndAddInt是调用unsafe中的方法。

public final int getAndAdd(int delta) {        return unsafe.getAndAddInt(this, valueOffset, delta);}
public final int getAndAddInt(Object var1, long var2, int var4) {
      int var5;    do {
          var5 = this.getIntVolatile(var1, var2);    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));    return var5;}

var5的赋值调用了 unsafe 的 getIntVolatile(var1, var2) 方法,这是一个native方法,作用是获取变量var1中偏移量var2出的值,这里传入的参数var1的值是对AtomicInteger对象的引用,var2就是AtomicInteger中存的数值的偏移量也就是value,所以得到的var5的值代表当前时刻下的原子类中存储的值。

代码中compareAndSwapInt方法的五个参数分别代表var1:object,var2:offset,var5:expectedValue,var5+var4:newValue。

第一个参数objec就是要修改的对象,传入的是this,也就是AtomicInteger这个对象。

第二个参数offset代表偏移量,借助它可以获取到value的数值。

第三个参数expectedValue代表期望值,传入的就是刚才获取的var5。

第四个参数newValue代表就是要修改为的值,等于之前得到的数值var5加上var4,而var4就是我们之前传入的delta,delta就是我们希望原子类改变的数值,可以传入+1,-1。

所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。

CAS操作成功,就会退出这个 while 循环,但是也有可能操作失败。如果操作失败就意味着在获取到 var5 之后,并且在 CAS 操作之前,value 的数值已经发生变化了,证明有其他线程修改过这个变量。

Unsafe中的getAndAddInt 方法通过循环+CAS的方式来实现,在这个过程中,通过compareAndSwapInt方法来尝试更新value的值,如果更新失败就重新获取,然后再次尝试,知道成功。

CAS问题

ABA问题

首先什么是ABA问题?ABA问题就是比如刚开始读取到的备份是3,然后被其他线程修改几次后,最终结果还是3,那么CAS很可能识别不到数据发生变化了,那么CAS 就会认为变量的值在此期间没有发生过变化。所以CAS并不能检测出在此期间值是不是被修改过,他只能检查测出现在的值和最初的只是不是一样的,这样就会有极大的隐患。

解决办法:可以通过添加版本号等标志位来解决,我们在这个值之外,再添加一个版本号,那么这个值的变化路径就从A->B->A变成了A1->B2->A3,这样一来就可以通过对比版本号来判断值是否发生过变化。

在atomic包中提供了AtomicStampedReference这个类,它是专门解决ABA问题的,解决思路也正是利用版本号,AtomicStampedReference会维护一种类似<Object,int>的数据结构,其中的int就是用于计数的,也就是版本号,它可以对这个对象和int版本号同时进行原子更新,从而也就解决了ABA问题,所以我们判断值是否被修改过,就通过版本号是否变化为标准,即使值一样,版本号也不一样。

循环问题

就像刚才说的,CAS可能会失败,而且CAS往往是配合着循环来实现的,失败就会一直重试,如果在CAS操作中一直失败,或者长时间自旋不成功,那么循环时间长开销大,会给CPU带来很大的开销。

解决方法:可以使用自适应自旋锁解决这个问题。

操作范围不能灵活控制

只能保证一个共享变量而不是多个共享变量的原子操作,这个对象可能是AtomicInteger、AtomicLong、或者其他类型的,但是我们不能针对多个共享变量同时进行CAS操作。比如AtomicInteger都是每次只对一个变量进行原子控制。

解决方法:通过利用一个新的类,来整合刚才这一组共享变量,这个类中的多个成员变量就是刚才那个共享变量,然后再利用atomic包中的AtomicReference来把这个新对象整体进行CAS操作,这样就可以保证线程安全。

总结

好了,今天主要讲解了关于CAS的介绍和优缺点,以及CAS的使用场景,CAS作为很多乐观锁的底层知识相当重要,理解CAS相关知识对学习并发编程相关概念和技能都有很大帮助!

03-09 09:06