CAS的概述
CAS的底层原理
Unsafe类+CAS思想(自旋锁)
CAS介绍
- CAS全称为Compare-And-Swap,它是一条cpu并发原语,比较工作内存值(预期值)和物理内存的共享值是否相同,相同则执行规定操作,否则继续比较,直到工作内存和主内存的值相同,这个过程是原子性的。
- Atomic相关的类主要是利用CAS、volatile和native方法来保证原子操作的,从而避免synchronized这样的重锁产生的高开销,执行效率大大提升。
- CAS并发原语体现在Java语言中的sun.misc包下的Unsafe类的各种方法,调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令,完全依赖于硬件功能,通过它实现了原子操作,再次强调,==由于CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。==
关于Unsafe.getAndIncrement()方法分析
CAS代码展示
java
/*
* CAS:Compare and swap [比较并交换]
* */
public class AtomicIntegerDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
//true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
//false 2019
System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
}
}
Unsafe类
Unsafe类的介绍
- Unsafe类是CAS的核心类,由于Java方法无法直接访问底层,是需要通过本地的native方法进行访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据,Unsafe类位于sun.misc包中,其内部方法可以像C的指针一样直接操作内存,因此Java中CAS操作依赖于Unsafe类的方法。
- Unsafe类中的所有方法都有native修饰,所以Unsafe类中的方法都可以直接操作底层资源,执行响应任务。
变量valueOffset
该变量是内存中的偏移地址,因为Unsafe类就是根据内存偏移地址来获取数据的。
下图中的value被volatile修饰,保证了多线程之间的可见性。
CAS的缺点
第一点
循环时间长的话,开销很大,可以看到getAndInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS一直不成功,可能给CPU带来很大的开销。
第二点
- 只能保证一个共享变量的原子性
- 当对一个共享变量执行操作时,可以使用循环CAS来保证原子性;
- 当对多个共享变量执行操作时,循环CAS就无法保证操作的原子性,这个时候,可以用锁来保证原子性。
第三点--ABA问题的产生(重点)
ABA问题说明
比如一个线程t1从内存位置V取出值A,这时候另一个线程t2也从内存中取出值A,并且线程t2进行了一些操作将值A变为值B,又将V位置的数据变回了值A,这时候t1进行CAS操作时发现内存中仍然是A,然后线程t1执行成功,尽管t1执行成功,并不代表过程没有问题。
ABA代码演示
java
//ABA问题
static AtomicInteger atomicInteger = new AtomicInteger(100);
private static void extracted() {
new Thread(() -> {
//先改为101
atomicInteger.compareAndSet(100, 101);
try {
//确保第一次修改完,往下执行
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//又改回100
atomicInteger.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
//确保线程1发生了ABA问题
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//结果:true 2022
System.out.println(atomicInteger.compareAndSet(100, 2022) + "\t" + atomicInteger.get());
}, "t2").start();
}
ABA问题解决方案
解决方法
ABA问题解决方案可以使用AtomicStampedReference,每修改一次都会有一个版本号。
解决方法代码演示
java
/**
* 初始值100,initialStamp版本号1
*/
static AtomicStampedReference asr = new AtomicStampedReference(100, 1);
public static void main(String[] args) {
new Thread(() -> {
//首次版本号
int stamp = asr.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
try {
//暂停下,让t4拿到的版本号和我一样
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp() + 1);
System.out.println(Thread.currentThread().getName() +
"\t" + "2次流水号:" + asr.getStamp() + "\t" + "修改为了:" + asr.getReference());
asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp() + 1);
System.out.println(Thread.currentThread().getName() +
"\t" + "3次流水号:" + asr.getStamp() + "\t" + "修改为了:" + asr.getReference());
}, "t3").start();
new Thread(() -> {
//首次版本号
int stamp = asr.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
try {
//确保线程3发生了ABA问题
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = asr.compareAndSet(100, 2022, stamp, stamp + 1);
System.out.println("修改状态:" + b + "\t" + "现在的值:"+asr.getReference() + "\t" + "现在的版本号:"+asr.getStamp());
}, "t4").start();
}
运行结果:
t3 首次版本号: 1
t4 首次版本号: 1
t3 2次流水号:2 修改为了:101
t3 3次流水号:3 修改为了:100
修改状态:false 现在的值:100 现在的版本号:3