CAS是什么?ABA问题的产生和解决方法
CAS是什么?
比较并交换(compare and swap)是一条CPU并发原语
功能
判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的,中间不予许中断,解决数据一致性问题。
底层原理
Unsafe类
是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。 Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的方法
注意 Unsafe中的所有方法都是native修饰的,就是说Unsafe中的方法都是直接操作系统底层资源执行任务
底层汇编
底层代码
// AtomicInteger类中方法:getAndIncrement,调用Unsafe类中的getAndAddInt
public final int getAndIncrement(){
return unsafe.getAndAddInt(this,valueOffset,1);
}
//Unsafe类中
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;
}
解释:
-
var1是AtomicInteger对象本身
-
var2是对象值的引用地址
-
var4是需要变动的数值
-
var5是通过var1、var2找出的内存中的真实的值 方法:用对象的值和var5作比较,如果相同,则更新var5+var4并且返回TRUE,如果不同则继续取值然后再比较,直到更新完成
缺点
1、 循环时间开销大
2、只能保证一个共享变量的原子操作
3、 引出ABA问题
ABA问题
CAS算法实现一个重要前提需要提取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化。
举例:
一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将A变成了B,然后又将V位置的数据变成了A,而这时候线程one进行 CAS操作的时候发现内存中仍然是A,然后one线程提示操作成功。
尽管one线程的CAS操作成功,但是不代表这个线程是没问题的
代码还原
package com.dayu.inter; import sun.rmi.runtime.NewThreadAction; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; /** * @Author: dayu * @Date: 2019/9/24 15:13 * @Description */ public class ABADemo { public static void main(String[] args) { System.out.println("=========下面是ABA问题的产生=========="); AtomicReference<Integer> atomicReference = new AtomicReference<>(100); new Thread(() -> { System.out.println(atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get()); System.out.println(atomicReference.compareAndSet(101, 100)+"\t"+atomicReference.get()); }, "t1").start(); new Thread(() -> { //休息一会,让线程t1先执行一遍ABA的问题 try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(atomicReference.compareAndSet(100, 2000)+"\t"+atomicReference.get()); }, "t2").start(); //休息一会,确保上面两个线程执行完毕 try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("=========下面是ABA问题的解决=========="); //有点类似于乐观锁 //初始值设定100,时间戳(版本号=1) AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次获取版本号"+stamp); //休息一会,等待t4获取版本号 try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第2次获取版本号"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第3次获取版本号"+atomicStampedReference.getStamp()); }, "t3").start(); //t4和t3最初获取到的版本号一致, new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次获取版本号"+stamp); //休息一会,确保t3完成一次ABA try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName()+"\t 是否修改成功:"+result+"\t 当前真实的版本号:"+atomicStampedReference.getStamp() +"\t 当前真实的值:"+atomicStampedReference.getReference()); }, "t4").start(); } }
为了解决ABA问题,在原子引用类上加上版本号,这个有点类似于mysql的乐观锁一样,每个线程更改一次都需要更改版本号,那么多线程同时获取到同一个版本号的时候也只有一个线程可以更改成功。