Java多线程与高并发专题原子类是如何利用-CAS-保证线程安全的
Java多线程与高并发专题——原子类是如何利用 CAS 保证线程安全的?
什么是原子类?原子类有什么作用?
在解答这个问题之前,我们需要先知道什么是原子类,以及它有什么作用。
在编程领域里,原子性意味着“一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分”。在Java中,原子类是java.util.concurrent.atomic包下提供的一类线程安全类,通过无锁算法 (如CAS) 实现原子操作,确保多线程环境下对共享变量的操作不可分割。其核心作用是可以原子性地执行添加、递增、递减等操作。比如之前多线程下的线程不安全的 i++ 问题,到了原子类这里,就可以用功能相同且线程安全的 getAndIncrement 方法来优雅地解决。
原子类的作用和锁有类似之处,都是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势:
- 粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。
- 效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程。
6 类原子类纵览
下面我们来看下一共有哪些原子类,原子类一共可以分为以下这 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:对int类型变量进行原子操作。
- AtomicLong:对long类型变量进行原子操作。
- AtomicBoolean:对boolean类型变量进行原子操作。
我们来介绍一下最为典型的 AtomicInteger,还是先看它的源码注释:
An int value that may be updated atomically. See the java. util. concurrent. atomic package specification for description of the properties of atomic variables. An AtomicInteger is used in applications such as atomically incremented counters, and cannot be used as a replacement for an Integer. However, this class does extend Number to allow uniform access by tools and utilities that deal with numerically-based classes.
翻译:
一个可以被原子更新的整数值。有关原子变量属性的详细描述,请参见 java.util.concurrent.atomic 包的规范说明。AtomicInteger 通常用于原子递增计数器等应用场景,它不能作为 Integer 的替代品。不过,该类确实继承了 Number 类,以便工具和实用程序能够对基于数值的类进行统一访问。
对于这个类型而言,它是对于 int 类型的封装,并且提供了原子性的访问和更新。也就是说,我们如果需要一个整型的变量,并且这个变量会被运用在并发场景之下,我们可以不用基本类型 int,也不使用包装类型 Integer,而是直接使用 AtomicInteger,这样一来就自动具备了原子能力,使用起来非常方便。
AtomicInteger 类常用方法
AtomicInteger 类有以下几个常用的方法:
/**
* 获取当前AtomicInteger对象的int值。
*
* @return 当前的int值
*/
public final int get() {
return value;
}
/**
* 将AtomicInteger对象的值设置为指定的新值。
*
* @param newValue 要设置的新值
*/
public final void set(int newValue) {
value = newValue;
}
/**
* 最终将AtomicInteger对象的值设置为指定的新值。
* 此方法不会立即刷新到主内存,可能会有延迟。
*
* @param newValue 要设置的新值
* @since 1.6
*/
public final void lazySet(int newValue) {
// 使用Unsafe类的putOrderedInt方法设置值,该方法不会立即刷新到主内存
unsafe.putOrderedInt(this, valueOffset, newValue);
}
/**
* 原子地将AtomicInteger对象的值设置为指定的新值,并返回旧值。
*
* @param newValue 要设置的新值
* @return 之前的int值
*/
public final int getAndSet(int newValue) {
// 使用Unsafe类的getAndSetInt方法原子地设置值并返回旧值
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**
* 如果当前AtomicInteger对象的值等于预期值,则原子地将其设置为新值。
*
* @param expect 预期的值
* @param update 要设置的新值
* @return 如果成功设置则返回{@code true},否则返回{@code false},表示实际值不等于预期值。
*/
public final boolean compareAndSet(int expect, int update) {
// 使用Unsafe类的compareAndSwapInt方法进行原子比较和交换操作
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
因为它本身是一个 Java 类,而不再是一个基本类型,所以要想获取值还是需要一些方法,比如通过 get方法就可以获取到当前的值。其中compareAndSet方法也是 CAS 的一个重要体现。
接下来的几个方法和它平时的操作相关:
/**
* 原子性地将当前值加 1,并返回更新前的值。
*
* 该方法利用 Unsafe 类的底层操作,确保在多线程环境下对值的更新是原子性的,
* 避免了多线程竞争导致的数据不一致问题。
*
* @return 更新前的值
*/
public final int getAndIncrement() {
// 调用 Unsafe 类的 getAndAddInt 方法,以原子方式将当前对象的 value 字段增加 1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* 原子性地将当前值减 1,并返回更新前的值。
*
* 此方法借助 Unsafe 类的底层机制,保证在多线程环境中对值的递减操作是原子性的,
* 从而避免了多线程并发访问时可能出现的数据竞争问题。
*
* @return 更新前的值
*/
public final int getAndDecrement() {
// 调用 Unsafe 类的 getAndAddInt 方法,以原子方式将当前对象的 value 字段减少 1
return unsafe.getAndAddInt(this, valueOffset, -1);
}
/**
* 原子性地将给定的值添加到当前值上,并返回更新前的值。
*
* 该方法通过 Unsafe 类的底层操作,确保在多线程环境下对值的加法操作是原子性的,
* 保证了数据的一致性和并发安全性。
*
* @param delta 要添加的值
* @return 更新前的值
*/
public final int getAndAdd(int delta) {
// 调用 Unsafe 类的 getAndAddInt 方法,以原子方式将当前对象的 value 字段增加 delta
return unsafe.getAndAddInt(this, valueOffset, delta);
}
这个参数就是我想让当前这个原子类改变多少值,可以是正数也可以是负数,如果是正数就是增加,如果是负数就是减少。而刚才的 getAndIncrement 和 getAndDecrement 修改的数值默认为 +1 或 -1,如果不能满足需求,我们就可以使用 getAndAdd 方法来直接一次性地加减我们想要的数值。
Atomic*Array 数组类型原子类
下面我们来看第二大类 AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。这样一来,我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray。
它一共分为 3 种,分别是:
- AtomicIntegerArray:对int数组进行原子操作。
- AtomicLongArray:对long数组进行原子操作。
- AtomicReferenceArray:对引用类型数组进行原子操作。
Atomic*Reference 引用类型原子类
下面我们介绍第三种 AtomicReference 引用类型原子类。
AtomicReference 类的作用和AtomicInteger并没有本质区别, AtomicInteger 可以让一个整数保证原子性,而AtomicReference 可以让一个对象保证原子性。这样一来,AtomicReference 的能力明显比 AtomicInteger 强,因为一个对象里可以包含很多属性。
在这个类别之下,除了 AtomicReference 之外,还有:
- AtomicStampedReference:它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
- AtomicMarkableReference:和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。
Atomic*FieldUpdater 更新器原子类
第四类我们将要介绍的是 Atomic\FieldUpdater,我们把它称为原子更新器。这些原子类用于对类的字段进行原子更新。它们提供了一种安全的方式,可以在不暴露字段的情况下进行原子操作。
- AtomicIntegerFieldUpdater:对int类型字段进行原子更新。
- AtomicLongFieldUpdater:对long类型字段进行原子更新。
- AtomicReferenceFieldUpdater:对引用类型字段进行原子更新。
如果我们之前已经有了一个变量,比如是整型的 int,实际它并不具备原子性。可是木已成舟,这个变量已经被定义好了,此时我们有没有办法可以让它拥有原子性呢?
办法是有的,就是利用Atomic*FieldUpdater,如果它是整型的,就使用AtomicIntegerFieldUpdater 把已经声明的变量进行更新,这样一来这个变量就拥有了 CAS 操作的能力。
这里的非互斥同步手段,是把我们已经声明好的变量进行 CAS 操作以达到同步的目的。
那么既然想让这个变量具备原子性,为什么不在一开始就声明为 AtomicInteger?这样也免去了更新的过程,难道是一开始设计的时候不合理吗?
这里有以下两种情况:
- 第一种情况是出于历史原因考虑,那么如果出于历史原因的话,之前这个变量已经被声明过了而且被广泛运用,那么修改它成本很高,所以我们可以利用升级的原子类。
- 另外还有一个使用场景,如果我们在大部分情况下并不需要使用到它的原子性,只在少数情况,比如每天只有定时一两次需要原子操作的话,我们其实没有必要把原来的变量声明为原子类型的变量,因为AtomicInteger 比普通的变量更加耗费资源。所以如果我们有成千上万个原子类的实例的话,它占用的内存也会远比我们成千上万个普通类型占用的内存高。所以在这种情况下,我们可以利用AtomicIntegerFieldUpdater 进行合理更新,节约内存。
Adder 累加器
累加器用于在多线程环境中进行高效的累加操作。
- LongAdder:对long类型变量进行累加操作。
- DoubleAdder:对double类型变量进行累加操作。
Accumulator 积累器
积累器用于在多线程环境中进行高效的积累操作,支持自定义的积累函数。
- LongAccumulator:对long类型变量进行积累操作。
- DoubleAccumulator:对double类型变量进行积累操作。
以 AtomicInteger 为例,分析在 Java 中如何利用 CAS 实现原子操作?
让我们回到标题中的问题,在充分了解了原子类的作用和种类之后,我们来看下 AtomicInteger 是如何通过 CAS 操作实现并发下的累加操作的,以其中一个重要方法 getAndAdd 方法为突破口。
这个方法的代码我们前面有展示,可以看到它主要是使用了 Unsafe 这个类,并且调用了 unsafe.getAndAddInt 方法。
而Unsafe 其实是 CAS 的核心类。
由于 Java 无法直接访问底层操作系统,而是需要通过 native 方法来实现。不过尽管如此,JVM 还是留了一个后门,在 JDK 中提供了 Unsafe 类,由它来实现硬件级别的原子操作,我们可以利用它直接操作内存数据。
那么我们就来看一下 AtomicInteger 的一些重要代码,如下所示:
/**
* 该类继承自Number类并实现了java.io.Serializable接口,用于表示一个可以原子性更新的整数。
* 原子性操作确保在多线程环境下,对该整数的读写操作是线程安全的。
*/
public class AtomicInteger extends Number implements java.io.Serializable {
... ...
// 设置使用Unsafe类的compareAndSwapInt方法进行原子性更新操作。
// Unsafe类提供了一些底层操作,如直接内存访问和原子性操作。
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 存储value字段在AtomicInteger类中的内存偏移量。
// 偏移量用于在原子性操作中定位value字段的内存地址。
private static final long valueOffset;
// 静态代码块,在类加载时执行,用于初始化valueOffset。
static {
try {
// 通过Unsafe类的objectFieldOffset方法获取value字段的内存偏移量。
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
// 如果获取偏移量时发生异常,抛出一个Error。
throw new Error(ex);
}
}
// 存储AtomicInteger的当前值,使用volatile关键字确保该变量在多线程环境下的可见性。
private volatile int value;
... ...
可以看出,在数据定义的部分,首先还获取了 Unsafe 实例,并且定义了 valueOffset。我们往下看到static 代码块,这个代码块会在类加载的时候执行,执行时我们会调用 Unsafe 的 objectFieldOffset 方法,从而得到当前这个原子类的 value 的偏移量,并且赋给 valueOffset 变量,这样一来我们就获取到了 value 的偏移量,它的含义是在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
value 是用 volatile 修饰的,它就是我们原子类存储的值的变量,由于它被 volatile 修饰,我们就可以保证在多线程之间看到的 value 是同一份,保证了可见性。
接下来继续看 Unsafe 的 getAndAddInt 方法的实现,代码如下:
/**
* 以原子方式将给定值添加到指定对象的指定偏移量处的整数值上,并返回该位置的旧值。
*
* 此方法利用了 CAS(Compare-And-Swap)操作来保证操作的原子性,避免了多线程环境下的数据竞争问题。
*
* @param var1 包含要修改的字段的对象。
* @param var2 对象内的偏移量,指定要操作的字段位置。
* @param var4 要添加到字段当前值的整数值。
* @return 操作之前字段的旧值。
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
// 用于存储当前字段的值
int var5;
do {
// 以 volatile 语义读取指定对象指定偏移量处的整数值
var5 = this.getIntVolatile(var1, var2);
// 循环尝试使用 CAS 操作更新值,直到成功
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// 返回更新前的旧值
return var5;
}
首先我们看一下结构,它是一个 do-while 循环,所以这是一个死循环,直到满足循环的退出条件时才可以退出。
那么我们来看一下 do 后面的这一行代码 var5 = this.getIntVolatile(var1, var2) 是什么意思。这是个native 方法,作用就是获取在 var1 中的 var2 偏移处的值。
那传入的是什么呢?传入的两个参数,第一个就是当前原子类,第二个是我们最开始获取到的 offset,这样一来我们就可以获取到当前内存中偏移量的值,并且保存到 var5 里面。此时 var5 实际上代表当前时刻下的原子类的数值。
现在再来看 while 的退出条件,也就是 compareAndSwapInt 这个方法,它一共传入了 4 个参数,这 4 个参数是 var1、var2、var5、var5 + var4,为了方便理解,我们给它们取了新了变量名,分别 object、offset、expectedValue、newValue,具体含义如下:
- 第一个参数 object 就是将要操作的对象,传入的是 this,也就是 atomicInteger 这个对象本身;
- 第二个参数是 offset,也就是偏移量,借助它就可以获取到 value 的数值;
- 第三个参数 expectedValue,代表“期望值”,传入的是刚才获取到的 var5;
- 而最后一个参数 newValue 是希望修改的数值 ,等于之前取到的数值 var5 再加上 var4,而 var4就是我们之前所传入的 delta,delta 就是我们希望原子类所改变的数值,比如可以传入 +1,也可以传入 -1。
所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。
一旦 CAS 操作成功,就会退出这个 while 循环,但是也有可能操作失败。如果操作失败就意味着在获取到 var5 之后,并且在 CAS 操作之前,value 的数值已经发生变化了,证明有其他线程修改过这个变量。
这样一来,就会再次执行循环体里面的代码,重新获取 var5 的值,也就是获取最新的原子变量的数值,并且再次利用 CAS 去尝试更新,直到更新成功为止,所以这是一个死循环。
我们总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。