cas——compareAndSwap

发布时间 2023-04-08 14:23:19作者: 李勇888

概念

  • compareAndSwap翻译过来就是 比较并交换
  • cas底层 调用的是unSafe,unSafed对底层的修改调用的native方法(CPU并发原语),天然原子性

代码说话

  • 创建一个AtomicInteger类,初始化值5,此时线程A去修改,把5读到工作内存,修改成2000,在写回主内存时,会比较当时拿到工作内存的5和现在主内存是否一致, 一致则修改成2000,不一致则表示被其他线程修改了。
  • 底层代码

  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;
    
    		/**
   * Atomically increments by one the current value.
   *
   * @return the previous value
   */
  public final int getAndIncrement() {
      return unsafe.getAndAddInt(this, valueOffset, 1);
  }
  
  // 这里会做自旋
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

  • 假设线程A和线程B同时执行getAndAddInt操作

    • 1 AtomicInteger里面的value原始值为3,即主内存中Atomicinteger的value=3,根据Jmm模型,线程A和线程B各自持有一份值为3的value的副本拷贝到各自的工作内存。

    • 2 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。

    • 3 线程B通过getIntVolatile(var1,var2)拿到value值3,此时线程B得到CPU执行权,执行compareAndSwapint方法比较内存值也为3,成功修改成4 ,此时主内存值为4了

    • 4 线程A恢复,执行compareSwapint方法比较,发现自己手里的值3和主内存不一致了,本次修改失败,重新读取再来一遍

    • 5 线程A重新获取value值,因为变量value被volatile修饰,是可见的,compareSwapint方法成功,修改成功。

UnSafe类的理解

  • UnSafe
    • 它是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsage相当于是一个后门,基于该类可以直接操作特定内存的数据。

    • Unsafe类存在于sun.misc包中,其 内部方法操作可以像C的指针一样直接操作内存

    • Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务

  • compareAndSwapInt
    • 该方法的实现位于unsafe.cpp中

    • 使用cmpxchg指令比较并更新值(保证原子性),下面是底层写法

      // obj 是当前工作内存的值
      // offset 是获取主内存的值
      oop p = JNIHandles::resolve(obj)
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset)
      (Atomic::cmpxchg(x, addr, e))
      

AtomicInteger是如何保证线程安全

  • 变量valueOffset 标识该变量值在内存中的 偏移地址,因为Unsafe就是根据内存 偏移地址 来获取数据的

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
      
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    
  • value用volatile修饰,保证了内存之间的可见性

    private volatile int value;
    
  • 总结

    • 利用CAS(底层是靠硬件层面的锁) + volatile来保证原子性,从而避免synchronized的高开销,提升执行效率

cas的缺点

  • 当前线程如果每次比较并交换的时候都返回false,就会一直请求,会给CPU带来很大的开销
  • ABA问题
    • 当线程A去读主内存的值为1, 另一个线程B修改了这个值为2并写回主内存,然后又重新修改回1写回主内存,然后线程A进行修改写回主内存是可以成功的
    • 尽管cas成功了,但是不代表这个过程是没问题的
    • 解决:
      • 通过添加版本号的方式
      • 如:AtomicReference会有ABA问题、AtomicStampedReference没有ABA问题

底层深究

  • CPU并发原语为什么就能保证原子性 (借鉴)
    • CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在他们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度

    • image

    • 现在都是多核 CPU 处理器,每个 CPU 处理器内维护了一块字节的内存,每个内核内部维护着一块字节的缓存,当多线程并发读写时,就会出现缓存数据不一致的情况

    • 总线锁定

      • 当一个处理器要操作共享变量时,在 BUS 总线上发出一个 Lock 信号,其他处理就无法操作这个共享变量了。

      • 缺点很明显,总线锁定在阻塞其它处理器获取该共享变量的操作请求时,也可能会导致大量阻塞,从而增加系统的性能开销。

    • 缓存锁定

      • 后来的处理器都提供了缓存锁定机制,也就说当某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现的。

        • MESI是Modified、Exclusive、Shared、Invalid这四个单词的首字母。这4个字母分别代表4种状态

          状态 描述 监听任务
          Modified 该缓存行有效,但是该缓存数据已经被当前核心修改,此时和DRAM中数据不一致。我们将其置为M,其他的核中缓存行都会置为I。 监听总线上所有对该缓存行写回DRAM的操作(不希望别人写入),需要将该操作延迟到自己将缓存行写回到主存后变成S状态。
          Excluside(互斥) 该缓存行有效,数据和RAM的数据一致,数据只存在当前内核工作内存中,只有他在使用是独占的。 监听总线上所有从DRAM读取该缓存行的操作,一旦有读的,需要将状态置为S状态。
          Shared(共享) 该缓存行有效,不过当前缓存行在多个核中都有,并且大家以及DRAM中的都一样。 监听其他的缓存中将该缓存置为I或者为E的事件,将状态置为I状态。
          Invalid(无效) 表明该缓存行无效,如果想要获取数据的话,就去DRAM中加载最新的。 不需要监听。
      • 现代的处理器基本都支持和使用的缓存锁定机制