CAS机制

First let’s look at atomic

  • 原子操作类,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。如 AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于 Boolean,Integer,Long 类型的原子性操作。在某些情况下,性能会比 synchronized 更好
  • 而 Atomic 操作类的底层正是用到了 “CAS 机制”

Basic concept

  • compare ans swap,即比较-替换,这个名称来源于unsafe类的一系列compareAndSwap***()方法。
  • CAS 机制中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。
  • 更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B并返回true,否则什么都不做并返回false。
  • CAS一定要volatile变量配合,这样才能保证可见性,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功
  • CAS在底层硬件级别一定是原子的,同时只有一个线程执行CAS操作

Underlying principle

  • 使用了Unsafe类
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;// 内存地址偏移量

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;//本类的几乎所有方法都是围绕它展开的
    ·······
    ·······
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}
  • 上面是AtomicInteger类的部分代码,其中使用了unsafe类
  • Unsafe 是 CAS 的核心类,存在于 sun.misc 包中,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问, Unsafe 类相当一个后门,基于该类可以像 C 指针一样直接操作特定内存的数据。Unsafe类中的大部分方法都是native修饰的,都可以直接调用OS底层资源
  • 变量 value 就是该原子操作类的值,用 volatile 修饰,保证了多线程之间的内存可见性。
  • 变量 vauleOffset非常重要,表示该变量值(即value,AtomicInteger.class.getDeclaredField(“value”)就是用于获取value值的)在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据的。

compareAndSet方法

  • compareAndSet方法的作用就是去执行CAS机制
  • 从下图可以看出AtomicInteger和unsafe的关系,AtomicInteger的compareAndSet方法调用了unsafe的CAS方法,即compareAndSwap***(),这个方法是用native修饰的,表示该方法是JNI,Java Native Interface,即Java调用C代码的接口,在IDEA里已经不能继续跟踪,因为它的实现被隐藏了
    在这里插入图片描述
  • 调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。
  • 也就是说CAS底层是一种CPU级别的并发原语。
  • 由于 CAS 是一种系统级源语,源语属于操作系统用语范畴,是由若干条指令组成,用于完成某一个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,不会造成数据不一致问题

incrementAndGet方法

  • 再来看AtomicInteger的另一个方法:incrementAndGet。这个方法的作用是Atomically increments by one the current value.(也就是在当前值的基础上+1,返回值是+1之后的值,还有一个getAndIncrement方法,差不多,只不过返回+1之前的值)
  • 它同样是调用了compareAndSwapInt这个native方法(其实AtomicInteger类几乎所有方法都调用了unsafe类,这里只是随便挑两个举例)。
  • 我们可以看到,在unsafe的getAndAddInt这个方法里,实际上体现了自旋的思想(通过do while)。如果compareAndSwapInt即返回true,即CAS执行成功,while循环就会退出,并返回修改之前的那个值即v,v是通过getIntVolatile获取的(也称为获取快照值),这也是个native方法。反之,如果真实值和期望值不同,那么就只能重新getIntVolatile,再次去进行CAS操作
    在这里插入图片描述
  • 那么compareAndSwapInt这个native方法在干什么呢?
  • 它就是调用了底层的C代码,利用offset即内存偏移量找到value在内存中的位置。然后应该就利用了底层的CAS汇编指令去执行CAS了
  • 这个偏移量怎么得到的?就在AtomicInteger类的static代码块中
static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));//通过这个方法得到
        } catch (Exception ex) { throw new Error(ex); }
    }
  • 为什么要放在static代码块里呢?因为Atomic类一旦被加载到内存,value的内存地址其实就固定了,获取偏移量就是为了获取value的内存地址,不管这个内存地址里是否有值(将来它被new了就有值了)
  • 而这个objectFieldOffset方法,可想而知又是一个native方法,因为操作内存Java代码是干不了的(还是C/C++好啊!)。如下:
/**
 * Report the location of a given static field, in conjunction with {@link
 * #staticFieldBase}.
 */
public native long objectFieldOffset(Field f);
  • 最后,再看看compareAndSwapInt的实现:
    在这里插入图片描述
  • 这段代码位于unsafe.cpp中,这是一段CPU原语,有时间再来研究。

Question:why use CAS rather than synchronized?

  • synchronized加锁,同时只能一个线程读写;CAS没有加锁(从源码可以看出来),而是通过CAS机制保障原子性,同时可以多个线程读
  • CAS在读多写少的情况下效率更高

CAS disadvantage?

  • 循环时间长开销很大
    • 如果 CAS 失败,会一直尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销(比如线程数很多,每次比较都是失败,就会一直循环),所以希望是线程数比较小的场景。
  • 只能保证一个共享变量的原子操作
    • 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
  • ABA 问题

What is CAS’s ABA problem?

AtomicReference类

  • 先来看看AtomicReference类,即原子引用类,这个类其实和AtomicInteger类大体上是一样的,只不过它的value是一个泛型,也就是说,这个类可以替代之前的Atomic***基本数据类型包装类,同时让你在操作自定义类的时候也可采用CAS机制。
  • 如下,是它的部分代码:
public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile V value;

基本操作:

User z3 = new User("z3", 22);
User li4 = new User("li4", 25);

AtomicReference<User> atomicReference = new AtomicReference<User>();
atomicReference.set(z3);
atomicReference.compareAndSet(z3, li4);//使用CAS机制

使用AtomicReference类演示ABA问题:

AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100); // 初始值 100
      new Thread(() -> {
         atomicReference.compareAndSet(100, 101);
         atomicReference.compareAndSet(101, 100);
      }).start();

      new Thread(() -> {
         try {
//          Thread.sleep(1000);
            TimeUnit.SECONDS.sleep(2); // 保证上面线程先执行
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         atomicReference.compareAndSet(100, 2019);
         System.out.println(atomicReference.get()); //输出为: 2019
      }).start();

AtomicStampedReference类

  • AtomicStampedReference类是AtomicReference类的“加强版”,就是为了解决ABA问题。它创建了一个内部类Pair,Pair包括了reference和stamp两个变量。部分代码如下:
public class AtomicStampedReference<V> {
    
    private static class Pair<T> {
        final T reference; // 即操作的泛型
        final int stamp; // 版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

Use AtomicStampedReference solve ABA problem

  • 思路其实就是加一个版本号stamp,每次修改值的同时版本号递增
  • ABA问题发生后,虽然值一样,但版本号不一样,就可以判断出现了ABA
AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(100, 1); // 初始版本为1

new Thread(() -> { // 用于产生ABA问题
   int stamp = asr.getStamp();
   System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
   try {
      Thread.sleep(1000);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp() + 1 ); // 版本号递增
   asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp() + 1 );
}).start();

new Thread(() -> {
   int stamp = asr.getStamp();
   System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
   try {
      Thread.sleep(3000);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   boolean b = asr.compareAndSet(100, 2019, stamp, stamp + 1);
   System.out.println(b); // false  即修改失败
   System.out.println(asr.getReference()); // 结果:100 
}).start();
  • compareAndSet方法的内部处理如下,逻辑比较好理解
  • 执行到最后会调用casPair这个方法,最后调用unsafe类的compareAndSwapObject这个JNI
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}


private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值