深入理解CAS

目录

  • 深入理解CAS
      • CAS中的引入
      • 什么是CAS?
      • CAS原理——Unsafe类
      • CAS优点
      • CAS缺点
      • ABA问题
      • 解决ABA问题

深入理解CAS

 

CAS中的引入

我们知道我们使用Volatile可以保证可见性,但不保证原子性,那么,如果我们不使用Lock锁和synchronized,我们该如何保证添加了volatile关键字的共享变量的原子性呢?

解决方案:我们可以使用Java中java.util.concurrent.atomic包下的原子类,来解决我们的原子性问题,而他们就是使用了CAS来实现的。
 

什么是CAS?

CAS的全称是:Compare And Swap(比较),CAS是CPU的并发原语,是CPU广泛支持的一种对内存中共享数据进行操作的一种特殊的指令,CAS可以将比较和交换转换成原子操作,这个原子操作由CPU保证。

 

CAS原理——Unsafe类

我们根据源码可知,我们的AtomicInteger类中有一个Unsafe类。我们知道Java是无法直接操作内存的,但是我们Java可以调用C++,而C++是可以操作内存的。比如我们的native 方法,而这个Unsafe类就是Java的后门,Unsafe类让Java拥有了像C语言的指针一样操作内存空间的能力,我们可以通过这个类来进行内存的操作。

我们的原子类AutomicInteger.incrementAndGet(),底层是调用了Unsafe类的getAndAddInt()方法

 
深入理解CAS_第1张图片
 
而Unsafe类中的getAndAddInt()方法底层使用的就是CAS,并且这个方法使用的是自旋锁。
CAS操作依赖3个值

  • 内存中的值V

  • 旧的预估值X

  • 要修改的值B

  首先自旋锁中的do代码块中,先会通过unsafe.getIntVolatile(Obj,ValueOffset),参数中传进去automicInteger对象、偏移量和1来获取内存中automicInteger中的value,然后内存中的value值赋值给var5,然后再根据自旋锁判断当前内存中的value值与var5的值进行比较,如果值相同,则将后面的var5+var4(1)赋值给value。如果值不同,则重新进入do代码块中获取内存中的value。

我们来举例一个多线程例子:比如有两条线程t1、t2,

  • t1:在通过getInvolatile()方法之后获取了内存中automicInteger中的value。
  • 而这个时候,t2:走了getAndAddInt()的整个流程,这个时候,内存中的值已经+1了
  • t1:来到while中判断,结果发现内存中的值和var5不相同,则重新走do这个流程重新获取内存中的内存值再赋值给var5,再重新判断。

这个流程就是CAS的原理。

 
深入理解CAS_第2张图片
 

 

CAS优点

  • 基于内存操作,效率很高。
  • 因为CAS底层使用的是自旋锁、乐观锁的思想,所以线程不会陷入阻塞状态,效率很高。

 

CAS缺点

CAS虽然能保证我们的原子性操作,但是会出出现以下的问题:
1、循环时间的开销较大:对于资源竞争较为激烈的场景,如有多个线程,这个时候CAS自旋的概率就会比较大,从而浪费CPU资源,效率可能会低于synchronized。
2、只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证的原子性,这个时候就可以用锁。
3、可能会出现ABA问题。
 

ABA问题

什么是ABA问题?
ABA问题一句话概括就是(狸猫换太子),假设两条线程去操作同一个资源,线程A和线程B,线程A想通过CAS把资源1改成2,而线程B拿到资源后把资源从1改为3,又把3改为1,而此时线程A还以为1是以前的资源,其实线程A拿到的值已经被线程B动过手脚了,这就是ABA问题。

 

解决ABA问题

1、我们可以使用原子引用AtomicRefence来解决ABA问题。

什么是原子引用AtomicReference?说白了原子引用就是带版本号的原子操作。
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(初始值,时间戳);
这个时间戳可以看成是版本号,如果我们把它设置为1,我们每次修改,我们都需要把这个值往上题,每次有人动了这个值,我们就知道这个值已经被人动过了,这根乐观锁是一样的原理。

在这里,如果我们使用的是 int 的包装类型 Integer ,那么我们就需要注意,我们的初始值不能设置大于128的,如果大于128的会出现CAS产生失败的现象。这是有一个大坑。

在阿里巴巴的开发手册中发现,Integer 采用了对象缓存机制,在-128至127之间赋值,Integer 对象会在IntegerCache.cache中获取,会复用已有的对象,但是这个区间以外的所有数据,都会在堆上产生,并不会复用已有的对象,这就导致了我们的引用不是同一个值,所有会修改失败。

 

 
 
 
  
  
  

你可能感兴趣的:(高并发,Java,CAS,ABA问题)