什么是CAS ?
CAS(Compare-And-Swap),字面意思 “比较并交换” 。
CAS相较于互斥锁是一种轻量级锁,并且是一种乐观锁。
乐观锁与悲观锁
悲观锁
悲观锁就是持悲观态度的锁。每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。
乐观锁
乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。
CAS三个操作数
内存位置、预期原值、更新值
执行CAS操作的时候,将内存位置的值与预期原值比较
如果匹配,那么处理器会自动将该位置值更新为新值
如果不匹配,处理器不做任何操作或重来
多个线程同时执行CAS操作只有一个会成功,这种重来的行为叫自旋。
如何使用CAS
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement()
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
CAS底层原理
CAS是JDK提供的非阻塞原子性操纵,它通过硬件保证了比较-更新的原子性,更加可靠。
它的底层是一条原子指令(cmpxchg),不会造成数据不一致问题,Unsafe类提供的CAS方法(compareAndSwapXXX)底层实现都是CPU指令cmpxchg。
执行cmpxchg的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个人线程会对总线枷锁成功,加锁成功后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,不起synchronized重量级锁,这里的排他时间要短很多,所以在多线程的情况下性能会更好。
对Unsafe的理解
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地方法来访问(native),Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部的方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。
Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
变量value用volatile修饰,保证了多线程之间的内存可见性。
ABA 问题(狸猫换太子)
线程1读取了数据A,线程2也读取了数据A。
线程2通过CAS比较,发现是原数据A没错,于是就将数据A改为了数据B。
线程3此时通过CAS比较,发现原数据就是数据B,于是就将数据B改成数据A。
此时,线程1通过CAS比较,发现原数据是A,就改成了自己要改的值。
虽然说线程1最后能能操作成功,但是这样已经违背了CAS的初衷,数据已经被修改过了,按CAS的原则来讲,CAS是不应该修改成功的。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger("A");
// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet("A", "B"));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet("B", "A"));
System.out.println(atomicInteger.get());
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet("A","C"));
System.out.println(atomicInteger.get());
}
}
如何解规避ABA问题
可以设置一个自增的标志位,数据的每一次修改标志位都会自增,比较标志位的值,还可以加上时间戳,显示上一次修改的时间,比较时间戳的值。
例如原子引用
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1,1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
// 乐观锁的原理相同!
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
}
CAS的缺点
- ABA问题
- 只能保证一个共享变量的原子操作
- 如果CAS自旋时间过长,会给CPU带来很大的开销