Java并发 之 Atomic 原子操作类

1,765 阅读3分钟

Atomic 原子操作类

在java.util.concurrent.atomic包里提供了一组原子操作类,这些类可以分成以下几种类别:

  • 基本类型: AtomicInteger、AtomicLong、AtomiBoolean;
  • 引用类型: AtomicReference、AtomicStampedReference、AtomicMarkableReference;
  • 数组类型: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;
  • 对象属性原子修改器: AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;
  • 原子类型累加器(jdk1.8新增): DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

众所周知,并发编程经常会遇到并发安全问题,最简单的例子就是多线程执行i++操作,就有可能获取不要正确的值,我们可以通过Synchronized关键字来进行控制达到线程安全的目的。但是Synchronized采用的是悲观锁策略,因此它并不是特别高效的解决方案。

而atomic包下提供的原子操作类都是操作简单、性能高效、并能保证线程安全的类,可以用它们更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。Atomic包下的这些类都是采用乐观锁策略去原子更新数据的,在java中的具体实现使用的是CAS操作。

基本类型原子更新

以AtomicInteger为例,总结基本类型的常用方法:

/** 将实例中的旧值更新为新值,并返回旧值
 * Atomically sets to the given value and returns the old value.
 * @param newValue the new value
 * @return the previous value
 */
public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**以原子的方式将实例中的原值加1,并返回自增前的旧值
 * Atomically increments by one the current value.
 * @return the previous value
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
/** 以原子的方式将实例中的原值加1,并且返回加1之后的值
 * Atomically increments by one the current value.
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/** 以原子方式将实例中的原值加上传入的参数值,并且返回相加后的结果
 * Atomically adds the given value to the current value.
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

使用示例:

public class AtomicIntegerDemo {

   static AtomicInteger atomicInteger = new AtomicInteger();

   public static void main(String[] args) throws InterruptedException {
      for (int i = 0; i < 10; i++){
         new Thread(() -> {
            for (int j = 0; j < 10000; j++){
               atomicInteger.incrementAndGet();
               System.out.println(Thread.currentThread().getName() + "current value:" + atomicInteger.get());
            }
         }, "Thread" + i).start();
      }

      Thread.sleep(3000);
      System.out.println("计算结果为:" + atomicInteger.get());
   }
}

从源码可以看出,incrementAndGet 是通过调用 unsafe.getAndAddInt()来实现的,再看getAndAddInt源码,我们可以知道这种原子操作是通过CAS自增实现的,如果CAS失败,会一直自旋直到成功则+1。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 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));
    // CAS 失败,则一直自旋
    return var5;
}

注意:在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,这种CAS失败自旋容易给CPU带来很大的开销

数组类型原子更新

以AtomicIntegerArray为例,总结数组类型常用方法

/** 将数组中索引为i的位置的元素进行更新
 * Atomically sets the element at position {@code i} to the given
 * updated value if the current value {@code ==} the expected value.
 * @param i the index
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
/** 以原子更新的方式将数组中索引为i的元素自增+1
 * Atomically increments by one the element at index {@code i}.
 * @param i the index
 * @return the previous value
 */
public final int getAndIncrement(int i) {
    return getAndAdd(i, 1);
}
/**以原子更新的方式将数组中索引为i的元素与输入值delta相加
 * Atomically adds the given value to the element at index {@code i}.
 * @param i the index
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int i, int delta) {
    return getAndAdd(i, delta) + delta;
}

使用示例:

public class AtomicIntegerArrayDemo {

   static int[] value = new int[]{1,2,3,4,5};
   static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);

   public static void main(String[] args) {
      //数组下标值为0的值更新为100
      atomicIntegerArray.set(0, 100);
      System.out.println(atomicIntegerArray.get(0));
      //数组下标值为1的值加5
      atomicIntegerArray.getAndAdd(1,5);
      System.out.println(atomicIntegerArray);
   }
}

引用类型原子更新

AtomicReference的作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性。

public class AtomicReferenceDemo {

   public static void main(String[] args) {
      User user1 = new User("用户1", 23);
      User user2 = new User("用户2", 25);
      User user3 = new User("用户3", 28);

      AtomicReference<User> atomicReference = new AtomicReference<>();

      //初始化为user1
      atomicReference.set(user1);
      System.out.println(atomicReference.get());
      //把user2 赋给 atomicReference, 成功
      atomicReference.compareAndSet(user1, user2);
      System.out.println(atomicReference.get());
      //把user3赋给atomicReference, 失败
      atomicReference.compareAndSet(user1, user3);
      System.out.println(atomicReference.get());
   }
}

class User{
   private String name;
   private Integer age;

   public User(String name, Integer age) {
   }

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }

   public Integer getAge() {
      return age;
   }

   public void setAge(Integer age) {
      this.age = age;
   }
}

对象属性原子修改器

AtomicIntegerFieldUpdater可以线程安全的更新对象中的整形变量 AtomicIntegerFieldUpdater的使用有一些限制条件如下:

  • 字段必须是volatile类型的,在线程之间共享变量时保证立即可见;
  • 字段的修饰符(public、protected、default、private)与调用者操作对象字段的关系一致。也就是说调用者能够之间操作对象字段,那么久可以反射进行原子操作。但是对于父类的字段,子类是不能之间操作的,尽管子类可以访问父类的字段。
  • 只能是实例变量,不能是类变量,也就是不能用static关键字
  • 只能是可修改变量,不能使用final修饰的变量,因为final的语义就是不可修改,实际上final和volatile是有冲突的,这两个关键字不能同时存在。
  • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型字段,不能修改其包装类型Integer/Long. 如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
public class AtomicIntegerFieldUpdateDemo {
   public static class Candidate {
      volatile int score = 0;
      AtomicInteger score2 = new AtomicInteger();
   }

   public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

   public static AtomicInteger realScore = new AtomicInteger(0);

   public static void main(String[] args) throws InterruptedException {
      final Candidate candidate = new Candidate();
      Thread[] threadArray = new Thread[10000];
      for (int i = 0; i < 10000; i++){
         threadArray[i] = new Thread(() -> {
            if (Math.random() > 0.4) {
               candidate.score2.incrementAndGet();
               scoreUpdater.incrementAndGet(candidate);
               realScore.incrementAndGet();
            }
         });
         threadArray[i].start();
      }

      for (int i = 0; i < 10000; i++){
         threadArray[i].join();
      }

      System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score);
      System.out.println("AtomicInteger Score=" + candidate.score2.get());
      System.out.println("realScore=" + realScore.get());

   }
}

原子类型累计器

LongAdder

引入LongAdder的初衷是,解决高并发环境下AtomicInteger,AtomicLong的自旋瓶颈问题。 低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。 性能测试示例:

public class LongAdderDemo {

   public static void main(String[] args) throws InterruptedException {
      testAtomicLongVSLongAdder(10, 10000);
      System.out.println("==================");
      testAtomicLongVSLongAdder(100, 200000);
      System.out.println("==================");
      testAtomicLongVSLongAdder(1000, 200000);
   }

   static void testAtomicLongVSLongAdder(final  int threadCount, final int times) throws InterruptedException {
      long start = System.currentTimeMillis();
      testLongAdder(threadCount, times);
      long end = System.currentTimeMillis() - start;
      System.out.println("线程数:" + threadCount + "; 单个线程操作的次数:" + times);
      System.out.println("testLongAdder总耗时:" + end);

      long start2 = System.currentTimeMillis();
      testAtomicLong(threadCount, times);
      long end2 = System.currentTimeMillis() - start2;
      System.out.println("线程数:" + threadCount + "; 单个线程操作的次数:" + times);
      System.out.println("testAtomicLong总耗时:" + end2);
   }

   static void testAtomicLong(final int threadCount, final int tiems) throws InterruptedException {
      CountDownLatch countDownLatch = new CountDownLatch(threadCount);
      AtomicLong atomicLong = new AtomicLong();
      for (int i=0; i < threadCount; i++){
         new Thread(() -> {
            for(int j = 0; j < tiems; j++){
               atomicLong.incrementAndGet();
            }
            countDownLatch.countDown();
         }, "Thread" + i).start();
      }
      countDownLatch.await();
   }

   static void testLongAdder(final int threadCount, final int times) throws InterruptedException {
      CountDownLatch countDownLatch = new CountDownLatch(threadCount);
      LongAdder longAdder = new LongAdder();
      for (int i=0; i < threadCount; i++){
         new Thread(() -> {
            for(int j = 0; j < times; j++){
               longAdder.add(1);
            }
            countDownLatch.countDown();
         }, "Thread" + i).start();
      }
      countDownLatch.await();
   }
}

LongAdder 原理

AtomicLong中有个内部变量value保存着long值,所有的操作都是针对该变量进行,也就是说,AtomicLong在高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。LongAdder的设计思路则是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就分散开了,冲突的概率就会小很多。如果要获取真正的long值,只需将各个槽中的变量值累加返回即可。

image.png

LongAdder 内部结构

LongAdder 内部有一个base变量,一个Cell 数组

  • base变量:非竞态条件下,直接累加到该变量上
  • Cell[]数组:竞态条件下,累加各个线程自己的Cell[i]槽

LongAdder 继承了Striped64,Striped64的部分源码如下:

/** Number of CPUS, to place bound on table size */
//CPU 核数,用来决定槽数组的大小
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * Table of cells. When non-null, size is a power of 2.
 */
 //槽数组,大小为2的次幂
transient volatile Cell[] cells;

/**
 * Base value, used mainly when there is no contention, but also as
 * a fallback during table initialization races. Updated via CAS.
 * 基数,在两种情况下会使用:
 * - 没有遇到并发竞争时,直接使用base累加数值
 * - 初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cells初始化)
 *   其他竞争失败的线程会将数值累加到base上
 */
transient volatile long base;

定义了一个Cell内部类,也就是槽,每个Cell对象存有一value值,通过Unsafe来CAS操作它的值。

/**
 * Padded variant of AtomicLong supporting only raw accesses plus CAS.
 *
 * JVM intrinsics note: It would be possible to use a release-only
 * form of CAS here, if it were provided.
 */
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

LongAdder中add方法的实现逻辑:

25318.png

  • 只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对Cell[]数组中单元Cell.
  • 如果Cell[]数组未初始化,会调用父类longAccumelate去初始化Cell[],如果Cell[]已经初始化,但是冲突发生在Cell单元内,则也调用父类的longAccumelate,此时可能就需要对Cell[]扩容了

LongAdder设计精妙之处就在于:尽量减少热点冲突,不到最后万不得已,尽量将CAS操作延迟。

Striped64中longAccumulate方法

25366.png

LongAdder中sum方法

源码如下:

/**
 *sum方法返回累加的和,是**当前时刻**的计数值。  
 *此返回值可能不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,方法的返回时刻和调用时
 *刻不是同一个点,在有并发的情况下,这个值只是近似准确的计数值。  
 *高并发时,除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值。
 * Returns the current sum.  The returned value is <em>NOT</em> an
 * atomic snapshot; invocation in the absence of concurrent
 * updates returns an accurate result, but concurrent updates that
 * occur while the sum is being calculated might not be
 * incorporated.
 *
 * @return the sum
 */
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;
}

由于计算总和时没有对Cell[]数组进行加锁,所以在累计过程中可能有其他线程对Cell中的值进行修改,也可能对数组进行了扩容,所有sum返回值并不是非常精确的,其返回值并不是一个调用sum方法时的原子快照值。

LongAccumulator

LongAccumulator是LongAdder的增强版。LongAdder只能针对数值的进行加减运算,而LongAccumulator提供了自定义的函数操作。其构造函数如下:

public LongAccumulator(LongBinaryOperator accumulatorFunction,
                       long identity) {
    this.function = accumulatorFunction;
    base = this.identity = identity;
}

通过LongBinaryOperator,可以自定义对传入参数的任意操作,并返回结果(LongBinaryOperator接收2个long作为参数,并返回1个long)。LongAccumulator内部原理和LongAdder几乎完全一样,都是利用了父类Striped64的longAccumulate方法。
示例:

public class LongAccumulatorDemo {

   public static void main(String[] args) throws InterruptedException {

      //累加x+y
      LongBinaryOperator accumulatorFunction = LongAccumulatorDemo::sumXY;
      LongAccumulator accumulator = new LongAccumulator(accumulatorFunction, 0);
      
      ExecutorService executorService = Executors.newFixedThreadPool(8);
      //1到9累加
      IntStream.range(1, 10).forEach(i -> executorService.submit(() -> accumulator.accumulate(i)));

      Thread.sleep(2000);
      System.out.println(accumulator.getThenReset());
   }

   static long sumXY(long x, long y){
      return x+y;
   }
}