【JUC】CAS(Compare And Swap)及其ABA问题
CAS和AtomicInteger
AtomicInteger用来保证自增原子性,它的实现是基于CAS(比较和交换)的。
CAS(CompareAndSwap):判断内存某个位置的值是否与预期值一致,如果是则更改为新值,这个过程是原子的。不会造成数据不一致的问题。
compareAndSet(except, update)方法: except是操作数据前从主内存中拿到的值,update是在工作内存中对变量拷贝副本进行修改的值,如果修改值之后,主内存的值还没有被改过,也就是except里的值,则可以将该值更新为update,并返回true。否则,返回false。
CAS的底层原理
自旋锁
上述代码:
- var1 AtomicInteger本身也就是this
- var2 该对象值的引用地址也就是valueOffset
- var4 需要变动的数量+X
- var5 用var1+var2找出来的真实的值
- 比较真实的值var5和对象当前的值var2 成功则var5 += var 4,并退出循环,
- 否则重新获取值,再比较。直到成功,退出循环。
【自旋锁的原理:自己循环比较直到相同】
线程A和B分别拷贝主内存中的值到自己的工作内存中,打算进行+1操作。
线程A通过getIntVolatile拿到value值为3,线程A被挂起。
线程B通过getIntVolatile拿到value值为3,线程B执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B任务完成。
线程A被唤起,执行compareAndSwapInt方法自己的值3和内存值4(volitale修改的可见性)比较,不一致,说明这个数据已经被修改了,线程A任务失败,线程A重新通过getIntVolatile拿到value值为4,线程A执行compareAndSwapInt方法比较内存值也为4,成功修改内存值为5。
UnSafe类
Unsafe类是CAS的核心类。Java方法无法直接访问底层系统,需要通过本地native方法来访问。Unsafe方法相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类在rt.jar/sun.misc包中,内部方法可以像C的指针一样直接操作内存。valueOffset存放内存中的偏移地址,Unsafe通过内存偏移地址来获取数据。
调用Unsafe类中的CAS方法,JVM会实现CAS汇编指令,实现原子操作,原语的执行必须是连续的,在执行过程中不允许被打断。CAS是CPU的原子指令,不会造成所谓的数据不一致问题。【Atomic::cmpxchg(x,addr,e)==e】
为什么不用synchronizated:因为他加锁之后只能让一个线程访问,虽然一致性得到了保证,但是并发性下降。
CAS的缺点是什么?
-
CAS基于自旋锁实现,它需要多次比较,循环时间长(比较成功才退出循环,否则一致循环),CPU开销大
- 只能保证1个共享变量的操作。多个共享变量就无法保证操作的原子性,只能通过加锁来保证。【Unsafe类的getAndAddInt传入的对象是this,只有一个对象】
- 存在ABA问题
什么是ABA问题?
CAS算法实现的前提是取出内存中某时刻的数据并在当下时刻比较并替换,这个时间差会导致数据的变化。
例如:线程1从内存中取出A,线程2也取出A,线程2操作后将值变成B,然后写回内存,然后又读取B将数据改成A,再写回内存。线程1进行CAS操作的时候发现内存中还是A,然后线程1操作成功。【内存中的值曾经变化过,但是线程1不知道】
CAS是一种乐观锁,它只关心头和尾,不关心过程中的变化情况。
ABA问题怎么解决?
- 使用AtomicReference原子引用来解决。
- AtomicReference增加一种机制,就是修改版本号,类似于时间戳。
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
参数:初始化值、版本号。
flag=stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
参数:期望的值、更新的值、期望的版本号、更新的版本号。
只有值和版本号都对应的时候才可以修改成功。
ABA问题的危害是什么?
在许多业务场景下,ABA问题并不会造成什么危害。
实际上在某些问题中,ABA问题也会造成数据不一致的问题。
比如在栈中从栈顶到栈底存放着ABCD四个元素。
线程1想要在栈顶为A的情况下,压入E,得到最后栈中从栈顶到栈底存放的是EBCD。
线程1首先查询栈顶是否为A,线程1获取到栈顶的值之后,被线程2中断。
线程2将ABCD依次弹出后,再压入A,此时栈中只剩下一个A。
线程1被唤醒,获取当前栈顶的值,仍然是A和之前获取到的值是一致的,所以可以操作,弹出A后压入E。
此时栈中仅仅只有E,而不是线程1想要的EBCD。