CAS是什么
Compare And Swap 比较并交换。CAS是一个cpu原语,该原子性操作不可被中断。
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
CAS原理
下面我们举一个例子来演示CAS原理
atomiclnteger.getAndIncrement();
相关参数介绍
1 Unsafe
Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。
Unsafe类中的方法主要是直接调用操作系统底层资源执行相应任务。
2 变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3 变量value
用volatile修饰,保证了多线程之间的内存可见性。
UnSafe.getAndAddInt()源码解释:
var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5是用过var1,var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。(非常占用cpu)
举例:
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行**compareAndSwapInt方法(CAS原语)**比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e))== e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址。
//通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
CAS缺点
1、占用过多CPU(循环取值比较)
// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
return var5;
}
当我们的预期值,与valueOffSet实际指向的值不同时,进行do-while循环,直到 预期值和内存中实际值 相同
2、CAS只能对一个变量进行原子性操作,而synchronized可以对一段代码进行锁操作
3、CAS存在ABA问题
存在两个线程t1,t2,同时访问共享变量A,将共享变量拷贝到线程自己的工作空间。由于t2处理速度快,可能直接修改A为B,再修改B为A。此时,如果t1要修改变量A,对变量A进行compareAndSwap。发现是A没变,直接修改。但是A其实已经被动过,A-B-A的过程可以发生很多事,A已经不再是原来那个单纯的A了
ABA问题的产生
存在两个线程t1,t2,同时访问共享变量A,将共享变量拷贝到线程自己的工作空间。由于t2处理速度快,可能直接修改A为B,再修改B为A。此时,如果t1要修改变量A,对变量A进行compareAndSwap。发现是A没变,直接修改。但是A其实已经被动过,A-B-A的过程可以发生很多事,A已经不再是原来那个单纯的A了
AtomicReference原子引用
之前我们了解过AtomicInteger是对Integer类型的共享变量,要保证数据一致性(原子性)问题时,采取的方案。
如果说我们需要将引用类型作为共享变量,保证原子性。可以使用AtomicReference。
class Person{
String name;
Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public class CASTest {
public static void main(String[] args) {
Person p1 = new Person("小明", 15);
Person p2 = new Person("小红", 16);
Person p3 = new Person("小张", 18);
// 共享资源p1
AtomicReference<Person> personAtomicReference = new AtomicReference<>(p1);
System.out.println("修改成功否" + personAtomicReference.compareAndSet(p1, p2));
System.out.println("修改成功否" + personAtomicReference.compareAndSet(p1, p3));
}
}
结果:
ABA问题解决(AtomicStampedReference)
public static void main(String[] args) {
AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("A",1);
AtomicReference<String> stringAtomicReference = new AtomicReference<>("A");
System.out.println("----------ABA 问题产生-----------");
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " 初始共享变量值为:"+stringAtomicReference.get());
stringAtomicReference.compareAndSet("A","B");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
stringAtomicReference.compareAndSet("B","A");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
},"t1").start();
new Thread(()->{
// 睡眠3秒确保 ,出现ABA问题
try { TimeUnit.MICROSECONDS.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); }
stringAtomicReference.compareAndSet("A","C");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
},"t2").start();
System.out.println("----------ABA 问题解决-----------");
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp());
// 睡眠一秒确保拿到初始值版本
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
stampedReference.compareAndSet("A","B",stamp,stamp+1);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp() +" 共享变量值为:"+stampedReference.getReference());
stampedReference.compareAndSet("B","A",stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp() +" 共享变量值为:"+stampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+stamp);
// 睡眠3秒确保 ,出现ABA问题,获取内存变量的版本号
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = stampedReference.compareAndSet("A", "C", stamp, stamp+1);
System.out.println("修改成功否 "+result);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+
stampedReference.getStamp() +
" 我的版本号为: "+ stamp +
" 共享变量值为:"+stampedReference.getReference());
},"t4").start();
}