简介

在编程领域里,原子性意味着“一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分”。而 java.util.concurrent.atomic 下的类,就是具有原子性的类,可以原子性地执行添加、递增、递减等操作。比如之前多线程下的线程不安全的 i++ 问题,到了原子类这里,就可以用功能相同且线程安全的 getAndIncrement 方法来优雅地解决。

原子类的作用和锁有类似之处,是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势:

  1. 粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。
  2. 效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程。

几种原子类

原子类一共可以分为以下这 6 类,如下表格所示:

类型类型具体类
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

Atomic 基本类型原子类

首先看第一类 Atomic*,我们把它称为基本类型原子类,它包括三种,分别是 AtomicInteger、AtomicLong 和 AtomicBoolean。

我们来学习一下最为典型的 AtomicInteger。对于这个类型而言,它是对于 int 类型的封装,并且提供了原子性的访问和更新。也就是说,我们如果需要一个整型的变量,并且这个变量会被运用在并发场景之下,我们可以不用基本类型 int,也不使用包装类型 Integer,而是直接使用 AtomicInteger,这样一来就自动具备了原子能力,使用起来非常方便。

AtomicInteger类常用方法

public final int get() //获取当前的值

因为它本身是一个 Java 类,而不再是一个基本类型,所以要想获取值还是需要一些方法,比如通过 get 方法就可以获取到当前的值。

public final int getAndSet(int newValue) //获取当前的值,并设置新的值

还有几个方法和平时的操作相关:

public final int getAndIncrement() //获取当前的值,并自增

public final int getAndDecrement() //获取当前的值,并自减

public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

这个参数就是我想让当前这个原子类改变多少值,可以是正数也可以是负数,如果是正数就是增加,如果是负数就是减少。而刚才的 getAndIncrement 和 getAndDecrement 修改的数值默认为 +1 或 -1,如果不能满足需求,我们就可以使用 getAndAdd 方法来直接一次性地加减我们想要的数值。

boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)

这个方法也是 CAS 的一个重要体现。

Array 数组类型原子类

第二大类 AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。这样一来,我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray。

它一共分为 3 种,分别是:

AtomicIntegerArray:整形数组原子类;

AtomicLongArray:长整形数组原子类;

AtomicReferenceArray :引用类型数组原子类。

AtomicReferenceArray 引用类型原子类

AtomicReference 引用类型原子类,AtomicReference 类的作用和AtomicInteger 并没有本质区别, AtomicInteger 可以让一个整数保证原子性,而AtomicReference 可以让一个对象保证原子性。这样一来,AtomicReference 的能力明显比 AtomicInteger 强,因为一个对象里可以包含很多属性。

在这个类别之下,除了 AtomicReference 之外,还有:

AtomicStampedReference:它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。

AtomicMarkableReference:和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。

Atomic\FieldUpdater 原子更新器

第四类我们将要介绍的是 Atomic\FieldUpdater,我们把它称为原子更新器,一共有三种,分别是。

AtomicIntegerFieldUpdater:原子更新整形的更新器;

AtomicLongFieldUpdater:原子更新长整形的更新器;

AtomicReferenceFieldUpdater:原子更新引用的更新器。

如果我们之前已经有了一个变量,比如是整型的 int,实际它并不具备原子性。可是木已成舟,这个变量已经被定义好了,此时我们有没有办法可以让它拥有原子性呢?办法是有的,就是利用 Atomic*FieldUpdater,如果它是整型的,就使用 AtomicIntegerFieldUpdater 把已经声明的变量进行升级,这样一来这个变量就拥有了 CAS 操作的能力。

这里的非互斥同步手段,是把我们已经声明好的变量进行 CAS 操作以达到同步的目的。那么你可能会想,既然想让这个变量具备原子性,为什么不在一开始就声明为 AtomicInteger?这样也免去了升级的过程,难道是一开始设计的时候不合理吗?这里有以下几种情况:

第一种情况是出于历史原因考虑,那么如果出于历史原因的话,之前这个变量已经被声明过了而且被广泛运用,那么修改它成本很高,所以我们可以利用升级的原子类。

另外还有一个使用场景,如果我们在大部分情况下并不需要使用到它的原子性,只在少数情况,比如每天只有定时一两次需要原子操作的话,我们其实没有必要把原来的变量声明为原子类型的变量,因为 AtomicInteger 比普通的变量更加耗费资源。所以如果我们有成千上万个原子类的实例的话,它占用的内存也会远比我们成千上万个普通类型占用的内存高。所以在这种情况下,我们可以利用 AtomicIntegerFieldUpdater 进行合理升级,节约内存。

看如下代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.concurrency;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterDemo implements Runnable{
static Score math;

static Score computer;

public static AtomicIntegerFieldUpdater<Score> 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();
AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
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);
}
}

这段代码就演示了这个类的用法,比如说我们有两个类,它们都是 Score 类型的,Score 类型内部会有一个分数,也叫作 core,那么这两个分数的实例分别叫作数学 math 和计算机 computer,然后我们还声明了一个 AtomicIntegerFieldUpdater,在它构造的时候传入了两个参数,第一个是 Score.class,这是我们的类名,第二个是属性名,叫作 score。

接下来我们看一下 run 方法,run 方法里面会对这两个实例分别进行自加操作。

第一个是 computer,这里的 computer 我们调用的是它内部的 score,也就是说我们直接调用了 int 变量的自加操作,这在多线程下是线程非安全的。

第二个自加是利用了刚才声明的 scoreUpdater 并且使用了它的 getAndIncrement 方法并且传入了 math,这是一种正确使用AtomicIntegerFieldUpdater 的用法,这样可以线程安全地进行自加操作。

接下来我们看下 main 函数。在 main 函数中,我们首先把 math 和 computer 定义了出来,然后分别启动了两个线程,每个线程都去执行我们刚才所介绍过的 run 方法。这样一来,两个 score,也就是 math 和 computer 都会分别被加 2000 次,最后我们在 join 等待之后把结果打印了出来,这个程序的运行结果如下:

普通变量的结果:1942 升级后的结果:2000

可以看出,正如我们所预料的那样,普通变量由于不具备线程安全性,所以在多线程操作的情况下,它虽然看似进行了 2000 次操作,但有一些操作被冲突抵消了,所以最终结果小于 2000。可是使用 AtomicIntegerFieldUpdater 这个工具之后,就可以做到把一个普通类型的 score 变量进行原子的自加操作,最后的结果也和加的次数是一样的,也就是 2000。可以看出,这个类的功能还是非常强大的。

Adder 加法器

Adder里面有两种加法器,分别叫作 LongAdder 和 DoubleAdder。

Accumulator 积累器

Accumulator 积累器也有 LongAccumulator 和 DoubleAccumulator两种。

CAS在原子类中的应用

接下来咱们以 AtomicInteger 为例,分析在 Java 中如何利用 CAS 实现原子操作?

咱们从getAndAdd方法的源码入手来分析:

1
2
3
4
5
6
7
8
9
  /**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

可以看出,里面使用了 Unsafe 这个类,并且调用了 unsafe.getAndAddInt 方法。所以这里需要简要介绍一下 Unsafe 类。

Unsafe 类

Unsafe 其实是 CAS 的核心类。由于 Java 无法直接访问底层操作系统,而是需要通过 native 方法来实现。不过尽管如此,JVM 还是留了一个后门,在 JDK 中有一个 Unsafe 类,它提供了硬件级别的原子操作,我们可以利用它直接操作内存数据。

关于CAS思想在原子类中的应用,之前在 CAS算法思想的应用场景 一文中有过详细介绍,本文不再赘述。

总结

我们对 6 类原子类进行了介绍,分别是 Atomic* 基本类型原子类、AtomicArray 数组类型原子类、AtomicReference 引用类型原子类、Atomic*FieldUpdater 升级类型原子类、Adder 加法器和 Accumulator 积累器。然后简要分析了CAS思想在原子类中的使用。