JUC:cas 算法、原子类、原子引用类

发布时间 2023-05-24 16:58:50作者: 黄光跃

什么是 CAS

  • 全称是 Compare-And-Swap,对数据进行 原子性 操作,sun.misc.Unsafe 类的各个 native 方法实现的
  • 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则什么都不做或者重来一次,重来就是自旋锁了 java各种锁看这里

CAS VS volatile VS synchronized

  • CAS:保证原子性
  • volatile:保证可见性和有序性
  • synchronized:保证原子性、可见性和有序性

volatile+CAS 可以做到 synchronized 做的事儿,意味着不需要 synchronized 这个重量级锁也能保证线程安全了
原子类是线程安全的,底层就是使用的 volatile+CAS

CAS 问题

synchronized 是悲观锁,CAS 是乐观锁,因此 CAS 通常情况下性能会更高。但是 CAS 并不是没有弊端

  • ABA 问题:值原来是 A 改成 B 再改成 A。最终是没有变化,但是过程中发生了变化
    • 解决方式:加个版本号之类的变量,A -> B -> C 变成 A1 -> B2 -> A3;类似数据库的乐观锁 version
    • JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决问题
  • CPU 开销:不自旋还好,如果需要自旋 cpu 会空转导致开销非常大(所以要自旋一定要考虑好自旋的次数,ReentrantLock底层休眠会释放CPU使用权,得到令牌后再唤醒线程,并不会空转)
  • 只能保证一个变量原子操作:JDK1.5 开始提供了原子引用来保证对象的原子操作(AtomicReference)变相的保证多个变量的原子性

原子类

java.util.concurrent.atomic 包下已经封装好的原子类(AtomicBoolean,AtomicLong,AtomicInteger 分别针对 bool,interger,long的原子类)

AtomicInteger atomicInteger = new AtomicInteger(1);
// 这里其实有三个步骤,获取原来的值、比较、赋新值;这个过程是原子性的,没有用 synchronized,底层调用了 Unsafe 的方法
atomicInteger.compareAndSet(1, 2);
atomicInteger.compareAndSet(3, 4);

原子引用类

java.util.concurrent.atomic 下除了提供基本数据类型的原子类,还提供了原子引用类 AtomicReference,用于封装自定义对象

AtomicReference<ShopDto> shopDtoAtomicReference = new AtomicReference<>();
ShopDto shopDto1 = new ShopDto();
shopDto1.setId(1L);
ShopDto shopDto2 = new ShopDto();
shopDto2.setId(2L);
shopDtoAtomicReference.set(shopDto1);
shopDtoAtomicReference.compareAndSet(shopDto1, shopDto2);

AtomicStampedReference 解决 aba 问题

内部维护了一个 Pair 内部类,初始化的时候同时维护 Pair,原理和数据库的乐观锁实现方式一致,带着版本号才能成功更新数据,常用的方法如下

方法 解释
public AtomicStampedReference(V initialRef, int initialStamp) 构造函数,初始化引用和版本号
public V getReference() 以原子方式获取当前引用值
public int getStamp() 以原子方式获取当前版本号
public V get(int[] stampHolder) 以原子方式获取当前引用值和版本号
public void set(V newReference, int newStamp) 以原子方式设置引用的当前值和新值,新引用值和新版本号只要有一个跟当前值不一样,就进行更新
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) 带着新的版本号和值更新,失败返回false

示例

// 第一个是值,第二个是版本号
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
// 获取版本号
int stamp = stampedReference.getStamp();
// 值改为 101,同事版本号+1
stampedReference.compareAndSet(100, 101, stamp, stamp+1);