一、什么是 CAS
一句话:比较并交换 == Compare and Swap
解释:一个线程在使用atomicInteger原子变量进行修改值的操作中,底层的CAS算法会拿自己工作空间的值去和主内存空间的值去比较,如果主内存值和期望数值5相同,则去修改为2019,否则修改失败。即CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。
二、CAS 的底层原理--Unsafe的理解
//相当于i++操作实现
public final int getAndIncrement(){
return unsafe.getAndInt(this, valueoffset, 1);
}
以atomicInteger为例,所使用的方法都是Unsafe类的方法,也就是CAS算法使用的是Unsafe类提供的方法。
什么是Unsafe:
Unsafe类是CAS的核心类,由于java方法无法访问底层系统,需要本地方法(native)来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存的数据,Unsafe存在于sun.misc包中,其内部的方法操作可以像C的指针一样直接操作内存,所以java中CAS操作的执行依赖于 Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应的任务。
三、CAS算法的缺点
1、循环时间长开销大
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们只能使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3、会出现ABA问题
四、什么是ABA问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化,也就是说两个线程都读到数据为5,一个线程暂停2秒后,另一个线程把5修改为6然后又修改回5,当第一个线程来到后发现和期望值相同,则修改想要修改的值。
尽管线程的CAS操作成功,但是不代表这个过程就是没问题的。
五、如何解决ABA问题
使用原子引用 + 新增时间戳(修改版本号)
代码演示ABA问题及解决:
/**
* ABA问题解决
* @author wannengqingnian
*/
public class TestAtomicStampedReference {
/**
* 创建带时间戳的原子引用
*/
static AtomicStampedReference<Integer> atomicStampedReference
= new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
//启动一个T1线程模拟ABA问题出现
new Thread(() -> {
//获取时间戳
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次时间戳" + stamp);
//暂停1秒钟T1线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模拟ABA
atomicStampedReference.compareAndSet(100, 101,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第一次修改版本号 : "+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次修改版本号 : "+atomicStampedReference.getStamp());
}, "T1").start();
//启动T2线程验证是否解决ABA问题
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t线程获得的版本号 :"+stamp);
//暂停3秒,确保T1完成ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//开始修改
Boolean flag = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改是否成功"+flag + "\t此时的版本号" + atomicStampedReference.getStamp());
}, "T2").start();
}
}
六、尾巴
关于CAS算法的介绍就是这些,如果有什么疑问或文章有问题,欢迎发消息告知。