什么是CAS

CAS(CompareAndSwap,比较并交换),通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与期望值是否相同,如果相同,就给它赋一个新值。
CAS可以看做是乐观锁(对比数据库的悲观、乐观锁)的一种实现方式,Java原子类中的递增操作就通过CAS自旋实现的。
CAS是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步
03-14 CAS(一) - 图1

CAS应用

CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作

  1. public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  2. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  3. public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

例如compareAndSwapInt 方法接收 4 个参数,分别是:对象实例、内存偏移量、字段期望值、字段新值。该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作。

  1. public class CASTest {
  2. public static void main(String[] args) {
  3. Entity entity = new Entity();
  4. Unsafe unsafe = UnsafeFactory.getUnsafe();
  5. long fieldOffset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
  6. System.out.println("fieldOffset:" + fieldOffset);
  7. boolean successful;
  8. // 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段新值
  9. successful = unsafe.compareAndSwapInt(entity, fieldOffset, 0, 3);
  10. System.out.println(successful + "\t" + entity.x);
  11. successful = unsafe.compareAndSwapInt(entity, fieldOffset, 3, 5);
  12. System.out.println(successful + "\t" + entity.x);
  13. //这里不会成功,因为上面cas已经把x的值改为5了
  14. successful = unsafe.compareAndSwapInt(entity, fieldOffset, 3, 7);
  15. System.out.println(successful + "\t" + entity.x);
  16. }
  17. }
  18. class UnsafeFactory {
  19. public static Unsafe getUnsafe() {
  20. try {
  21. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  22. field.setAccessible(true);
  23. return (Unsafe) field.get(null);
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. return null;
  28. }
  29. public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
  30. try {
  31. return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
  32. } catch (Exception e) {
  33. throw new Error(e);
  34. }
  35. }
  36. }
  37. class Entity {
  38. public int x;
  39. }

截屏2022-03-13 20.23.35.png

CAS缺陷

  • 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
  • 只能保证一个共享变量原子操作
  • ABA 问题

    cas的自旋是怎样实现的

    就是类似这样,用个do while或者for(;;)循环,用当前取到的值和内存中真实的值做比较,如果不一致,则把新的内存值赋值给var5,然后继续比较,直到能把新值写入成功为止。
    截屏2022-03-14 23.09.51.png

ABA问题

什么是ABA问题

当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功
03-14 CAS(一) - 图4

  1. public static void main(String[] args) {
  2. AtomicInteger atomicInteger = new AtomicInteger(1);
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. int value = atomicInteger.get();
  7. //阻塞1s
  8. LockSupport.parkNanos(1000000000L);
  9. if (atomicInteger.compareAndSet(value, 3)) {
  10. System.out.println("thread1 update from " + value + " to 3");
  11. } else {
  12. System.out.println("thread1 update fail");
  13. }
  14. }
  15. }, "thread1").start();
  16. new Thread(new Runnable() {
  17. @Override
  18. public void run() {
  19. int value = atomicInteger.get();
  20. //在这里先把value修改为2
  21. if (atomicInteger.compareAndSet(value, 2)) {
  22. System.out.println("thread2 update from " + value + " to 2");
  23. value = atomicInteger.get();
  24. System.out.println("thread2 read value:" + value);
  25. //在这里再把value修改为1
  26. if (atomicInteger.compareAndSet(value, 1)) {
  27. System.out.println("thread2 update from " + value + " to 1");
  28. }
  29. }
  30. }
  31. }, "thread2").start();
  32. }

上面代码可以看出来,thread2已经对value修改过2次,但是thread1还是能修改成功,是因为cas只是判断你最终的值是否和我的预期值一致,一致就能修改,不管你过程有修改过多少次。
截屏2022-03-14 01.14.14.png

ABA问题的解决方案

数据库有个锁 称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。
同样,Java也提供了相应的原子引用类AtomicStampedReference
截屏2022-03-13 20.31.24.png
reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增

  1. public static void main(String[] args) {
  2. // Pair.reference(变量)值为1, Pair.stamp(版本)为1
  3. AtomicStampedReference atomicStampedReference = new AtomicStampedReference(1, 1);
  4. new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. int[] stampHolder = new int[1];
  8. int value = (int) atomicStampedReference.get(stampHolder);
  9. int stamp = stampHolder[0];
  10. System.out.println("thread1 read value:" + value + ",stamp:" + stamp);
  11. // 阻塞1s
  12. LockSupport.parkNanos(1000000000L);
  13. if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
  14. System.out.println("thread1 update " + value + " to 3");
  15. } else {
  16. System.out.println("thread1 update fail");
  17. }
  18. }
  19. }, "thread1").start();
  20. new Thread(new Runnable() {
  21. @Override
  22. public void run() {
  23. int[] stampHolder = new int[1];
  24. int value = (int) atomicStampedReference.get(stampHolder);
  25. int stamp = stampHolder[0];
  26. System.out.println("thread2 read value:" + value + ",stamp:" + stamp);
  27. if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
  28. System.out.println("thread2 update " + value + " to 2");
  29. value = (int) atomicStampedReference.get(stampHolder);
  30. stamp = stampHolder[0];
  31. System.out.println("thread2 read value:" + value + ",stamp:" + stamp);
  32. if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
  33. System.out.println("thread2 update " + value + " to 1");
  34. }
  35. }
  36. }
  37. }, "thread2").start();
  38. }

可以看到这时候的thread1修改不成功,就算这时候的期望值和内存值都一致。因为AtomicStampedReference有个版本stamp的概念,提供了类似乐观锁的功能,会去比较版本号。
截屏2022-03-14 01.35.29.png