java.util.concurrent.atomic 不只有 AtomicInteger。按被更新的数据来看,可以分成基本类型、数组、引用和字段更新器几组。

基本类型与数组

AtomicInteger、AtomicLong、AtomicBoolean 分别用于对应值的原子访问。getAndIncrement() 返回旧值,incrementAndGet() 返回更新后的值,这个顺序很容易看反。

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicValueDemo {
    public static void main(String[] args) {
        AtomicInteger value = new AtomicInteger(10);
        System.out.println(value.getAndIncrement());
        System.out.println(value.incrementAndGet());
        int[] original = {1, 2};
        AtomicIntegerArray array = new AtomicIntegerArray(original);
        array.addAndGet(0, 5);
        System.out.println(array.get(0));
        System.out.println(original[0]);
    }
}

依次输出 10、12、6、1。通过数组构造 AtomicIntegerArray 时,会复制传入的内容,不是直接把原数组包装成原子数组。

每个元素的操作是原子的,但连续更新两个元素仍是两次操作。需要维持整个数组的一致快照时,要另外设计同步方式。

原子引用

AtomicReference<T> 更新的是引用本身,适合整体替换一个状态对象。它不会让对象内部所有字段都自动线程安全。

例如状态同时包含数量和金额,可以先根据旧状态构建一个新的不可变对象,再 CAS 替换引用。失败后重新读取和计算。不要在替换前直接修改旧对象,否则其他线程早就能看到那些修改,CAS 失败也无法撤回。

带版本和标记的引用分别是 AtomicStampedReference、AtomicMarkableReference,用于需要额外状态参与比较的情况。

字段更新器

已有对象中只想原子更新某个字段,可以使用字段更新器:

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class FieldUpdaterDemo {
    public volatile int count;
    private static final AtomicIntegerFieldUpdater<FieldUpdaterDemo> UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(FieldUpdaterDemo.class, "count");

    public static void main(String[] args) {
        FieldUpdaterDemo demo = new FieldUpdaterDemo();
        System.out.println(UPDATER.incrementAndGet(demo));
        System.out.println(demo.count);
    }
}

输出两次 1。更新器依赖字段名称和访问权限;整数字段要是 volatile 的 int,不是 Integer,也不能是 static 字段。

这种方式可以让很多对象共用一个更新器,减少每个对象再包装一个原子对象的需要。但所有复合更新都必须遵守同一套规则,其他代码直接写 count++,依然会破坏正确性。

不要把原子操作拼成非原子流程

先 get() 判断库存大于零,再 decrementAndGet(),两步之间可能已经被其他线程修改。原子类保证的是具体方法的操作,不是任意几行代码组合后的业务流程。

可以用 CAS 循环实现带条件的扣减,也可以用锁。涉及多个对象、多项检查时,锁往往更容易表达完整约束。

最后修改:2018 年 02 月 18 日
如果觉得我的文章对你有用,请随意赞赏