原子类

一、原子类纵览

类型 具体类
Atomic* 基本类型原子类 AtomicInteger、AtomicLong、AtomicBoolean
Atomic*Array 数组类型原子类 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
Atomic*Reference 引用类型原子类 AtomicReference、AtomicStampedReference、AtomicMarkableReference
Atomic*FieldUpdater 升级类型原子类 AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
Adder 累加器 LongAdder、DoubleAdder
Accumulator 积累器 LongAccumulator、DoubleAccumulator
  • AtomicInteger 类常用方法
    1. public final int get() //获取当前的值
    2. public final int getAndSet(int newValue) //获取当前的值,并设置新的值
    3. public final int getAndIncrement() //获取当前的值,并自增
    4. public final int getAndDecrement() //获取当前的值,并自减
    5. public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    6. boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)
  • Atomic*Array 数组类型原子类
    1. AtomicIntegerArray:整形数组原子类
    2. AtomicLongArray:长整形数组原子类
    3. AtomicReferenceArray :引用类型数组原子类
  • Atomic*Reference 引用类型原子类
    1. AtomicStampedReference:它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
    2. AtomicMarkableReference:和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。
  • Atomic*FieldUpdater 原子更新器
    如果我们之前已经有了一个变量,比如是整型的 int,实际它并不具备原子性。可是木已成舟,这个变量已经被定义好了,此时我们有没有办法可以让它拥有原子性呢?办法是有的,就是利用 Atomic*FieldUpdater,如果它是整型的,就使用 AtomicIntegerFieldUpdater 把已经声明的变量进行升级,这样一来这个变量就拥有了 CAS 操作的能力。
    public class AtomicIntegerFieldUpdaterDemo implements Runnable{
       static Score math;
       static Score computer;
       public static AtomicIntegerFieldUpdater scoreUpdater 
          = AtomicIntegerFieldUpdater.newUpdater(Score.class, "score");
    
       @Override
       public void run() {
           for (int i = 0; i < 1000; i++) {
               computer.score++;
               scoreUpdater.getAndIncrement(math);
           }
       }
    
       public static class Score {
           volatile int score;
       }
    
       public static void main(String[] args) throws InterruptedException {
           math =new Score();
           computer =new Score();
           AtomicIntegerFieldUpdaterDemo2 r 
               = new AtomicIntegerFieldUpdaterDemo2();
           Thread t1 = new Thread(r);
           Thread t2 = new Thread(r);
           t1.start();
           t2.start();
           t1.join();
           t2.join();
           System.out.println("普通变量的结果:"+ computer.score);
           System.out.println("升级后的结果:"+ math.score);
       }
    }
    
    1. AtomicIntegerFieldUpdater:原子更新整形的更新器
    2. AtomicLongFieldUpdater:原子更新长整形的更新器
    3. AtomicReferenceFieldUpdater:原子更新引用的更新器
  • Adder 加法器
    它里面有两种加法器,分别叫作 LongAdder 和 DoubleAdder。
  • Accumulator 积累器
    最后一种叫 Accumulator 积累器,分别是 LongAccumulator 和 DoubleAccumulator。

二、以 AtomicInteger 为例,分析在 Java 中如何利用 CAS 实现原子操作?

  • getAndAdd方法
    //JDK 1.8实现
    public final int getAndAdd(int delta) {
       return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    
    可以看出,里面使用了 Unsafe 这个类,并且调用了 unsafe.getAndAddInt 方法。所以这里需要简要介绍一下 Unsafe 类。
  • Unsafe
    Unsafe 其实是 CAS 的核心类。由于 Java 无法直接访问底层操作系统,而是需要通过 native 方法来实现。不过尽管如此,JVM 还是留了一个后门,在 JDK 中有一个 Unsafe 类,它提供了硬件级别的原子操作,我们可以利用它直接操作内存数据。
    public class AtomicInteger extends Number 
       implements java.io.Serializable {
      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 int get() {return value;}
      ...
    }
    
    1. 首先还获取了 Unsafe 实例,并且定义了 valueOffset
    2. static 代码块,这个代码块会在类加载的时候执行,执行时我们会调用 Unsafe 的 objectFieldOffset 方法,从而得到当前这个原子类的 value 的偏移量,并且赋给 valueOffset 变量,这样一来我们就获取到了 value 的偏移量,它的含义是在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
    3. value 是用 volatile 修饰的,它就是我们原子类存储的值的变量,由于它被 volatile 修饰,我们就可以保证在多线程之间看到的 value 是同一份,保证了可见性。
  • Unsafe 中的 getAndAddInt 方法
    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;
    }
    
    1. 首先我们看一下结构,它是一个 do-while 循环,所以这是一个死循环,直到满足循环的退出条件时才可以退出。
    2. do 后面的这一行代码 var5 = this.getIntVolatile(var1, var2) 这是个 native 方法,作用就是获取在 var1 中的 var2 偏移处的值。
    3. 传入的两个参数,第一个就是当前原子类,第二个是我们最开始获取到的 offset,这样一来我们就可以获取到当前内存中偏移量的值,并且保存到 var5 里面。此时 var5 实际上代表当前时刻下的原子类的数值。
    4. while 的退出条件,也就是 compareAndSwapInt 这个方法,它一共传入了 4 个参数,这 4 个参数是 var1、var2、var5、var5 + var4,为了方便理解,我们给它们取了新了变量名,分别 object、offset、expectedValue、newValue,具体含义如下:
      • 第一个参数 object 就是将要操作的对象,传入的是 this,也就是 atomicInteger 这个对象本身
      • 第二个参数是 offset,也就是偏移量,借助它就可以获取到 value 的数值
      • 第三个参数 expectedValue,代表“期望值”,传入的是刚才获取到的 var5
      • 最后一个参数 newValue 是希望修改的数值 ,等于之前取到的数值 var5 再加上 var4,而 var4 就是我们之前所传入的 delta,delta 就是我们希望原子类所改变的数值,比如可以传入 +1,也可以传入 -1
      • 所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程

三、AtomicInteger 和 AtomicLong 存在的问题


每一个线程是运行在自己的 core 中的,并且它们都有一个本地内存是自己独用的。在本地内存下方,有两个 CPU 核心共用的共享内存。

对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。

这样一来,每一次它的数值有变化的时候,它都需要进行 flush 和 refresh。比如说,如果开始时,ctr 的数值为 0 的话,那么如图所示,一旦 core 1 把它改成 1 的话,它首先会在左侧把这个 1 的最新结果给 flush 到下方的共享内存。然后,再到右侧去往上 refresh 到核心 2 的本地内存。这样一来,对于核心 2 而言,它才能感知到这次变化。

由于竞争很激烈,这样的 flush 和 refresh 操作耗费了很多资源,而且 CAS 也会经常失败。

  • LongAdder 带来的改进和原理
    1. LongAdder 引入了分段累加的概念,内部一共有两个参数参与计数:第一个叫作 base,它是一个变量,第二个是 Cell[] ,是一个数组。
    2. 其中的 base 是用在竞争不激烈的情况下的,可以直接把累加结果改到 base 变量上。
    3. 当竞争激烈的时候,就要用到我们的 Cell[] 数组了。一旦竞争激烈,各个线程会分散累加到自己所对应的那个 Cell[] 数组的某一个对象中,而不会大家共用同一个。
    4. LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,这是一种分段的理念,提高了并发性,这就和 Java 7 的 ConcurrentHashMap 的 16 个 Segment 的思想类似。
    5. LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中,就大大减少了刚才的 flush 和 refresh,以及降低了冲突的概率,这就是为什么 LongAdder 的吞吐量比 AtomicLong 大的原因,本质是空间换时间,因为它有多个计数器同时在工作,所以占用的内存也要相对更大一些。
    6. 那么 LongAdder 最终是如何实现多线程计数的呢?答案就在最后一步的求和 sum 方法,执行 LongAdder.sum() 的时候,会把各个线程里的 Cell 累计求和,并加上 base,形成最终的总和。代码如下:
      public long sum() {
         Cell[] as = cells; Cell a;
         long sum = base;
         if (as != null) {
             for (int i = 0; i < as.length; ++i) {
                 if ((a = as[i]) != null)
                     sum += a.value;
             }
         }
         return sum;
      }
      
    7. 在这个 sum 方法中可以看到,思路非常清晰。先取 base 的值,然后遍历所有 Cell,把每个 Cell 的值都加上去,形成最终的总和。由于在统计的时候并没有进行加锁操作,所以这里得出的 sum 不一定是完全准确的,因为有可能在计算 sum 的过程中 Cell 的值被修改了。
  • AtomicLong 可否被 LongAdder 替代
    不能,得区分场景
    LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一,而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。

四、AtomicInteger 和 synchronized 的异同点

  • 原理不同
    1. synchronized 背后的 monitor 锁,也就是 synchronized 原理,同步方法和同步代码块的背后原理会有少许差异,但总体思想是一致的:在执行同步代码之前,需要首先获取到 monitor 锁,执行完毕后,再释放锁。
    2. 原子类保证线程安全的原理是利用了 CAS 操作。
  • 使用范围不同
    1. synchronized 既可以修饰一个方法,又可以修饰一段代码,相当于可以根据我们的需要,非常灵活地去控制它的应用范围
    2. 对于原子类而言,它的使用范围是比较局限的。因为一个原子类仅仅是一个对象,不够灵活,仅有少量的场景,例如计数器等场景,我们可以使用原子类
  • 粒度的区别
    原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。通常情况下,synchronized 锁的粒度都要大于原子变量的粒度。如果我们只把一行代码用 synchronized 给保护起来的话,有一点杀鸡焉用牛刀的感觉。
  • 性能区别
    1. synchronized 是一种典型的悲观锁,悲观锁的操作相对来讲是比较重量级的。因为 synchronized 在竞争激烈的情况下,会让拿不到锁的线程阻塞,但是悲观锁的开销是固定的,也是一劳永逸的。随着时间的增加,这种开销并不会线性增长
    2. 原子利用的是乐观锁,永远不会让线程阻塞,虽然在短期内的开销不大,但是随着时间的增加,它的开销也是逐步上涨的

五、Java 8 中 Adder 和 Accumulator 有什么区别

  • Adder 的介绍
    对于 Adder 而言,比如最典型的 LongAdder,在高并发下 LongAdder 比 AtomicLong 效率更高,因为对于 AtomicLong 而言,它只适合用于低并发场景,否则在高并发的场景下,由于 CAS 的冲突概率大,会导致经常自旋,影响整体效率。
    而 LongAdder 引入了分段锁的概念,当竞争不激烈的时候,所有线程都是通过 CAS 对同一个 Base 变量进行修改,但是当竞争激烈的时候,LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,从而提高了并发性。
  • Accumulator 的介绍
    Accumulator 和 Adder 非常相似,实际上 Accumulator 就是一个更通用版本的 Adder,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。
    public class LongAccumulatorDemo {
        public static void main(String[] args) 
                                            throws InterruptedException {
            // 首先新建了一个 LongAccumulator,同时给它传入了两个参数
            LongAccumulator accumulator 
                               = new LongAccumulator((x, y) -> x + y, 0);
            // 然后又新建了一个 8 线程的线程池
            ExecutorService executor 
                               = Executors.newFixedThreadPool(8);
            // 利用整形流也就是 IntStream 往线程池中提交了从 1 ~ 9 这 9 个任务
            IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
            Thread.sleep(2000);
            System.out.println(accumulator.getThenReset());
        }
    }
    
    1. 这段代码的运行结果是 45,代表 0+1+2+3+...+8+9=45 的结果
    2. LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0); 我们传入了两个参数:
      • 第一个参数是二元表达式;
      • 第二个参数是 x 的初始值,传入的是 0。在二元表达式中,x 是上一次计算的结果(除了第一次的时候需要传入),y 是本次新传入的值
    3. 当执行 accumulator.accumulate(1) 的时候,首先要知道这时候 x 和 y 是什么,第一次执行时, x 是 LongAccumulator 构造函数中的第二个参数,也就是 0,而第一次执行时的 y 值就是本次 accumulator.accumulate(1) 方法所传入的 1;然后根据表达式 x+y,计算出 0+1=1,这个结果会赋值给下一次计算的 x,而下一次计算的 y 值就是 accumulator.accumulate(2) 传入的 2,所以下一次的计算结果是 1+2=3
    4. IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i))); 这一行语句中实际上利用了整型流,分别给线程池提交了从 1 ~ 9 这 9 个任务,相当于执行了:
      accumulator.accumulate(1);
      accumulator.accumulate(2);
      accumulator.accumulate(3);
      ...
      accumulator.accumulate(8);
      accumulator.accumulate(9);
    5. 那么根据上面的这个推演,就可以得出它的内部运行,这也就意味着,LongAccumulator 执行了:
      0+1=1;
      1+2=3;
      3+3=6;
      6+4=10;
      10+5=15;
      15+6=21;
      21+7=28;
      28+8=36;
      36+9=45;
    6. 这里需要指出的是,这里的加的顺序是不固定的,并不是说会按照顺序从 1 开始逐步往上累加,它也有可能会变,比如说先加 5、再加 3、再加 6。但总之,由于加法有交换律,所以最终加出来的结果会保证是 45。这就是这个类的一个基本的作用和用法。
    7. 拓展功能
      我们继续看一下它的功能强大之处。举几个例子,刚才我们给出的表达式是 x + y,其实同样也可以传入 x * y,或者写一个 Math.min(x, y),相当于求 x 和 y 的最小值。同理,也可以去求 Math.max(x, y),相当于求一个最大值。根据业务的需求来选择就可以了。代码如下:
      LongAccumulator counter = new LongAccumulator((x, y) -> x + y, 0);
      LongAccumulator result = new LongAccumulator((x, y) -> x * y, 0);
      LongAccumulator min = new LongAccumulator((x, y) -> Math.min(x, y), 0);
      LongAccumulator max = new LongAccumulator((x, y) -> Math.max(x, y), 0);
    8. 在这里为什么不用 for 循环呢?
      确实,用 for 循环也能满足需求,但是用 for 循环的话,它执行的时候是串行,它一定是按照 0+1+2+3+...+8+9 这样的顺序相加的,但是 LongAccumulator 的一大优势就是可以利用线程池来为它工作。一旦使用了线程池,那么多个线程之间是可以并行计算的,效率要比之前的串行高得多。这也是为什么刚才说它加的顺序是不固定的,因为我们并不能保证各个线程之间的执行顺序,所能保证的就是最终的结果是确定的。

你可能感兴趣的:(原子类)