深入理解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()方法
而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优点
- 基于内存操作,效率很高。
- 因为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中获取,会复用已有的对象,但是这个区间以外的所有数据,都会在堆上产生,并不会复用已有的对象,这就导致了我们的引用不是同一个值,所有会修改失败。
最近和朋友们在讨论CAS有没有锁,我的理解仅仅在于cas底层的自旋锁和乐观锁思想,实践上面有没有锁还不清楚。
CAS底层Native的实现(很深了)
如果我们看过OpenJDK的源代码的话,我们可以知道unsafe.cpp这个类,在unsafe.cpp这个类中,unsafe的cas方法实际上是调用了Atomic类的cmpxchg(汇编指令)方法,需要注意的是这里的atomic类的实现是跟操作系统有关,跟CPU的架构也有关。如果是Window X86系统的架构,cmpxchg(汇编指令)的单条CPU指令前缀是LOCK开头的,Lock前缀开头的指令会对CPU总线和高速缓存加锁,可以理解为CPU指令级别的一种锁。
所以实际物理上的实现也是有锁的。