【Java】多线程 - 原子类(Atomic Classes)

Posted by 西维蜀黍 on 2019-02-27, Last Modified on 2021-09-21

背景

我们知道 volatile 关键字虽然轻量级,但不能保证原子性,synchronized 可以保证原子性,但是比较重量级。

那么有没有一种简单的、性能高的方法来保证 Java 的原子操作呢?答案当然是有的。

在 JDK1.5 时期, Java 家族加入了 Atomic 包。

原子类(Atomic Classes)

Java 中提供了对应的原子操作类来实现原子地且有条件进行的读-改-写操作操作。其本质是利用了 CPU 级别的原子指令。由于原子类是基于 CPU 级别的指令,因此其开销比需要操作系统参与的锁的开销小。

CAS 算法

CAS (比较与交换,Compare and swap) 算法是一种非阻塞算法(non-blocking algorhithm),同时也是一种无锁算法(lock-free algorhithm),基于乐观并发(pessimistic concurrent control)的思想。

非阻塞版本相对于基于锁的版本有几个性能优势。首先,它用硬件的原生形态代替 JVM 的锁定代码路径,从而在更细的粒度层次上(独立的内存位置)进行同步,失败的线程也可以立即重试,而不会被挂起后重新调度。更细的粒度降低了争用的机会,不用重新调度就能重试的能力也降低了争用的成本。即使有少量失败的 CAS 操作,这种方法仍然会比由于锁争用造成的重新调度快得多。

以 Atomic 类中的 incrementAndGet() 方法为例,其内部就调用了Unsafe中的 native 方法(CompareAndSet)以实现递增数值:

private volatile int value;
public final int get() {
        return value;
    }
/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

我们来分析下incrementAndGet的逻辑:

  1. 先获取当前的value值
  2. 对value加一
  3. 第三步是关键步骤,调用compareAndSet方法来进行一个原子更新操作,这个方法的语义是:先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

AtomicInteger 例子

比如,AtomicInteger 表示一个 int 类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的 int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子地添加、递增和递减等方法。

AtomicInteger 的使用方法如下:

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}

原子基本类型

原子基本类型,从名称上就可以看出,是为基本类型提供原子操作的类。它们是以下3位:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

我们来看看 AtomicInteger 中的一些常用方法。

  • int getAndSet(int newValue):以原子方式更新,并且返回旧值。
  • int getAndIncrement():以原子方式自增,返回的是自增前的值。
  • int addAndGet(int delta):以原子方式,将当前值与输入值相加,返回的是计算后的值。

原子数组

下面的类是为数组中某个元素的更新提供原子操作的类。

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

我们来看看 AtomicIntegerArray 中的一些常用方法:

  • int getAndSet(int i, int newValue):更新对应位置的值,返回更新前的值。
  • boolean compareAndSet(int i, int expect, int update):比较对应位置的值与期望值,如果相等,则更新,返回true。如果不能返回false。
  • int getAndIncrement(int i):对位置i的元素以原子方式自增,返回更新前的值。
  • int getAndAdd(int i, int delta):对位置i的元素以原子方式计算,返回更新前的值。

原子引用类型

如果我们只需要某个类里的某个字段,也就是说让普通的变量也享受原子操作,可以使用原子更新字段类,如在某些时候由于项目前期考虑不周全,项目需求又发生变化,使得某个类中的变量需要执行多线程操作,由于该变量多处使用,改动起来比较麻烦,而且原来使用的地方无需使用线程安全,只要求新场景需要使用时,可以借助原子更新器处理这种场景,Atomic并发包提供了以下三个类:

  • AtomicReference
  • AtomicReferenceFieldUpdater
    • FieldUpdater方便以原子方式更新对象中的字段,字段不需要声明为原子变量,FieldUpdater是基于反射机制实现的。
  • AtomicMarkableReference

原子更新字段类

前文提到了AtomicReferenceFieldUpdater类,它更新的是类的字段,除了这个类,Atomic还提供了另外三个类用于更新类中的字段:

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicStampedReference

Reference