[译]Java Concurrent Atomic Package详解

翻译自:Package java.util.concurrent.atomic

地址:
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html#package.description
翻译JDK8而不是12的原因是JDK8 对于内存语义部分讲解更加详细。

包简介与类摘要

Package java.util.concurrent.atomic:一个支持对单个变量进行无锁、线程安全编程的小型工具类集合。

类摘要:

类名 描述
AtomicBoolean 可以原子性更新的boolean值。
AtomicInteger 可以原子性更新的int值。
AtomicIntegerArray 一个int数组,其中的元素可以原子性更新。
AtomicIntegerFieldUpdater 基于反射,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong 可以原子性更新的long值。
AtomicLongArray 一个long数组,其中的元素可以原子性更新。
AtomicLongFieldUpdater 基于反射,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference 可以原子性更新的对象引用
AtomicReferenceArray 一个对象引用数组,其中的元素可以原子性更新。
AtomicReferenceFieldUpdater<T,V> 基于反射,可以对指定类的指定 volatile reference 字段进行原子更新。
AtomicStampedReference 维护带有整数版本标志的对象引用,可以原子方式对其进行更新。
DoubleAccumulator 一个或多个共同维护“运行中”的 double 值的变量,该值由给定的函数更新。
DoubleAdder 一个或多个共同维护初始为零的 double 总和的变量。
LongAccumulator 一个或多个共同维护“运行中”的 long 值的变量,该值由给定的函数更新。
LongAdder 一个或多个共同维护初始为零的 long 总和的变量。

在JDK 8实现中,这四个类均继承Striped64。低竞争时更新base,高竞争时把更新分散到多个Cell,主要通过CAS降低热点竞争,并不是给每个线程分配独占元素或降低锁的粒度。sum等汇总读取也不保证是并发更新时的原子快照。

原子更新操作:compareAndSet

本质上,该package下的类扩展了volatile值、字段、数组元素的概念,提供以下形式的原子条件更新操作:

1
boolean compareAndSet(expectedValue, updateValue);

该方法(在不同类中的参数类型不同)原子地将变量设置为updateValue,如果它当前持有expectedValue,更新成功返回true。该包中的类还包含获取和无条件设置(set)值的方法。

这些方法的规范使实现能够采用当代处理器上可用的高效机器级原子指令。然而,在某些平台上,支持可能需要某种形式的内部锁定。因此,这些方法不是严格保证是非阻塞的(线程可能在执行操作之前暂时阻塞)。

AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference类的实例都提供对对应类型的单个变量的访问和更新。每个类还提供了适用于该类型的实用方法。例如,类AtomicLong和AtomicInteger提供原子增量方法。 一个应用是生成序列号,如:

1
2
3
4
5
6
class Sequencer {
private final AtomicLong sequenceNumber = new AtomicLong(0);
public long next() {
return sequenceNumber.getAndIncrement();
}
}

像 getAndIncrement 一样,定义新的、对某个值原子地应用函数的实用方法也很简单。例如,给定如下转换函数:

1
long transform(long input)

可以这样编写你的实用方法:

1
2
3
4
5
6
7
8
long getAndTransform(AtomicLong var) {
long prev, next;
do {
prev = var.get();
next = transform(prev);
} while (!var.compareAndSet(prev, next));
return prev; // transformAndGet 则返回 next
}

原子访问的内存语义与 lazySet

原子性访问和更新内存效果,与volatiles遵循同样的规则,如The Java Language Specification (17.4 Memory Model)所述:

  • get与volatile读效果一样
  • set与volatile写效果一样
  • lazySet具有release写入语义:它之前的普通读写不能被重排到它之后,但它与后续普通操作之间的约束比volatile写弱。
  • compareAndSet和所有其他读取和更新操作(如getAndIncrement)一样,与volatile变量读取和写入具有相同的内存效果。

JDK 8的AtomicReference.lazySet使用Unsafe.putOrderedObject,整数类使用对应的putOrdered方法。它在部分平台上比完整volatile写便宜,但不能概括为“JIT不会重排任何指令”,也没有“几纳秒后可见”的保证;同一线程的后续读取仍须遵守程序语义。具体屏障和成本取决于平台。
一个典型用途是把非阻塞数据结构中已不再使用的引用设为null,减少对象滞留。如果其他线程暂时看到旧引用也无妨,lazySet可能比set更合适;对象能否被GC回收还取决于是否有其他引用。

添加lazySet方法的原因:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6275329

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
As probably the last little JSR166 follow-up for Mustang,
we added a "lazySet" method to the Atomic classes
(AtomicInteger, AtomicReference, etc). This is a niche
method that is sometimes useful when fine-tuning code using
non-blocking data structures. The semantics are
that the write is guaranteed not to be re-ordered with any
previous write, but may be reordered with subsequent operations
(or equivalently, might not be visible to other threads) until
some other volatile write or synchronizing action occurs).

The main use case is for nulling out fields of nodes in
non-blocking data structures solely for the sake of avoiding
long-term garbage retention; it applies when it is harmless
if other threads see non-null values for a while, but you'd
like to ensure that structures are eventually GCable. In such
cases, you can get better performance by avoiding
the costs of the null volatile-write. There are a few
other use cases along these lines for non-reference-based
atomics as well, so the method is supported across all of the
AtomicX classes.

For people who like to think of these operations in terms of
machine-level barriers on common multiprocessors, lazySet
provides a preceeding store-store barrier (which is either
a no-op or very cheap on current platforms), but no
store-load barrier (which is usually the expensive part
of a volatile-write).
  • JDK 8的weakCompareAndSet允许伪失败,且不提供happens-before保证;它与compareAndSet的内存语义不同。

JDK 9起,AtomicInteger、AtomicLong、AtomicReference等类中原名为weakCompareAndSet的方法被弃用,因为名称容易让人误以为它具有volatile语义。可按需要选择weakCompareAndSetPlain、Acquire、Release或Volatile;不是整个包里所有同名方法都被弃用。

其它原子类与使用限制

除了表示单个值的类之外,package中还包含Updater类,可用于在类的volatile字段上执行compareAndSet操作。 AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater和AtomicLongFieldUpdater是基于反射的,可提供对相关字段类型的访问。这些主要用在这样的原子数据结构中:同一节点的几个 volatile 字段(例如树节点的链接)各自独立地接受原子更新。这些类在如何以及何时使用原子更新方面提供了更大的灵活性,但代价是更加笨拙的基于反射的设置、更不方便的使用和更弱的保证。

AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray类进一步将原子操作支持扩展到这些类型的数组。这些类还提供了数组元素的volatile访问语义,这是普通数组不支持的。

AtomicMarkableReference类将单个布尔值与引用相关联。 例如,该位可能在数据结构中使用,表示被引用的对象在逻辑上已被删除。 AtomicStampedReference类将整数值与引用相关联。 例如,这可以用于表示与一系列更新相对应的版本号。

原子类主要设计为用于实现非阻塞数据结构和相关基础结构类的构建。 compareAndSet方法不是锁定的一般替代方法。 仅当对象的关键更新仅限于单个变量时,它才适用。

原子类不是java.lang.Integer和相关类的通用替换。它们没有定义equals,hashCode和compareTo等方法(由于原子变量预期会发生变化,所以它们不适合作为哈希表键)。

此外,该包只为在预期应用中有用的常见类型提供类。例如,没有表示 byte 的原子类。在少数需要的场合,你可以用 AtomicInteger 保存 byte 值,并做适当的强制转换。

你还可以使用 Float.floatToRawIntBits(float) 和 Float.intBitsToFloat(int) 转换来保存 float,使用 Double.doubleToRawLongBits(double) 和 Double.longBitsToDouble(long) 转换来保存 double。

后续会出文章解析:AtomicLong、Striped64、LongAdder
本文的目的主要是从大体上了解atomic及其内存语义

JDK 12
https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/concurrent/atomic/package-summary.html


📚 系列文章 · Java 并发编程