【Java 多线程】5 - 6 深入了解原子包
§5-6 深入了解原子包
5-6.1 原子包简介
官方文档中对于原子包的介绍:
原子包 java.util.concurrent.atomic
是并发包中的一个子包,该包是一个小型工具包,提供了免锁、线程安全的单变量类。
每个 AtomicBoolean
, AtomicInteger
, AtmoicLong
和 AtomicReference
类实例都提供了对应类型变量的访问和更新。每个类也为对应类型提供了适当的工具方法。例如,AtomicLong
和 AtomicInteger
提供了原子自增方法。其中一种应用是生成数字序列,如下:
class Sequencer {
private final AtomicLong sequenceNumber = new AtomicLong(17);
public long next() {
return sequenceNumber.getAndIncrement();
}
}
所含值的任意变换由两种方式提供,一种是低级的读取-修改-写入操作(如 compareAndSet
),另一种是高级方法(如 getAndUpdate
)。
这些类都不能够处于一般目的替换 java.lang.Integer
和有关类。他们并没有定义诸如 equals
, hashCode
和 compareTo
方法。因为原子变量可能会变化,不适于用作哈希表的键。
AtomicIntegerArray
, AtomicLongArray
以及 AtomicReferenceArray
类进一步延展了原子操作,使得类支持对应类型的数组操作。值得注意的是,这些类对数组元素提供了 volatile
访问语义。
除了表示单变量和数组的类,改包还包含了更新器类(Updater
),可用于获取 compareAndSet
以及对任意所选类中的任意所选 volatile
字段上的相关操作。这些类早于 VarHandle
,其使用更为局限。AtomicReferenceFieldUpdater
, AtomicIntegerFieldUpdater
和 AtomicLongFieldUpdater
是基于反射的工具,提供对有关字段类型的访问。这主要在同一结点中(例如树节点的链接)多个 volatile
字段都各自依赖于原子更新的原子数据结构中更加有用。这些类在何时以及怎样使用原子更新方面,提供了更好的灵活性,但需要更为笨拙的基于反射的设置、更不便的使用,和更弱的保障。
AtomicMarkableReference
类将一个布尔值与一个引用关联。例如,该比特位可能用于一个数据结构内部,表示引用的对象逻辑上已被删除。AtomicStampedReference
类将一个整型与一个引用关联。这可能用于,例如,表示一系列更新的对应版本号。
概要:
自 JDK 1.5 起,Java 提供了原子包,这个高中的原子操作类提供了一种用法简单、性能高效、线程安全的更新变量的方式。根据变量类型,可将该包中的类分为四种类型:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段)、累加器。如下表所示:
分类 | 类 |
---|---|
原子更新基本类型 | AtomicBoolean ,AtomicInteger , AtomicLong |
原子更新引用类型 | AtomicMarkableReference<V> , AtomicReference<V> , AtomicStampedReference<V> |
原子更新数组类型 | AtomicIntegerArray , AtomicLongArray , AtomicReferenceArray<E> |
原子更新对象属性 | AtomicIntegerFieldUpdater<T> , AtomicLongFieldUpdater<T> , AtomicReferenceFieldUpdater<T,V> |
下文内容部分参考自:
5-6.2 原子更新基本类型
原子更新基本类型主要有布尔型、整型、长整型,每一个实例都表示一个可以原子更新的值。
由于原子更新基本类型的三个类用法几乎一模一样,这里仅以 AtomicInteger
为例,介绍其用法、从内存和源码分析。
构造方法:
构造方法 | 描述 |
---|---|
AtomicInteger() |
创建一个新的原子更新整型,初始值为 0 |
AtomicInteger(int initialValue) |
创建一个新的原子更新整型,并给定初始值 |
常用方法:
方法 | 描述 |
---|---|
int get() |
返回当前值 |
int getAndIncrement() |
以原子方式自增,并返回自增前的值 |
int getAndDecrement() |
以原子方式自减,并返回自减前的值 |
int getAndAdd(int delta) |
以原子方式相加,并返回相加前的值 |
int incrementAndGet() |
以原子方式自增,并返回自增后的值 |
int decrementAndGet() |
以原子方式自减,并返回自减后的值 |
int addAndGet(int delta) |
以原子方式相加,并返回相加后的值 |
void set(int newValue) |
设置新的值 |
示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
//原子更新基本类型演示
incrementPrimitives();
}
private static void incrementPrimitives() throws InterruptedException {
AtomicInteger primitive = new AtomicInteger();
System.out.println("原子更新基本类型:以整型为例");
new Thread(() -> {
for (int i = 0 ; i < 1000; i++) {
primitive.incrementAndGet();
}
}).start();
new Thread(() -> {
for (int i = 0 ; i < 1000; i++) {
primitive.incrementAndGet();
}
}).start();
Thread.sleep(1000);
System.out.println("最终结果:" + primitive);
}
}
运行结果:
原子更新基本类型:以整型为例
最终结果:2000
结构:以 AtomicInteger
为例,看看它的结构。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value; // 存储所维护的整型变量
// 方法...
}
5-6.3 原子更新引用类型
原子更新引用类型和原子更新基本类型一样,每个实例都只维护一个可以原子更新的引用类型变量。
原子更新引用类型有三种:AtomicReference
(原子更新引用)、AtomicMarkableReference
(带标记的原子更新引用类型)、AtomicStampedReference
(带版本号的原子更新引用类型)。
其中,AtomicMarkableReference
使用一个布尔标记关联引用,AtomicStampedReference
将一个整型数值与引用关联起来,作为数据原子更新的版本号,用于解决 CAS 算法中的 ABA 问题。
构造方法:
构造方法 | 描述 |
---|---|
AtomicReference() AtomicReference(V initialValue) |
使用 null 初值/给定初值创建一个新的原子更新引用 |
AtomicMarkableReference(V initialRef, boolean initialMark) |
使用给定初值创建一个新的原子更新标记引用 |
AtomicStampedReference(V initialRef, int initialStamp) |
使用给定初值创建一个新的原子更新戳引用 |
常用方法:
常用方法 | 描述 |
---|---|
V get() |
返回当前值 |
V getAndSet(V newValue) |
将引用原子更新为指定新值,并返回旧值 |
void set(V newValue) |
将引用原子更新为指定新值 |
boolean compareAndSet(V expectedValue, V newValue) |
若当前值等于期望值,则原子更新该引用为指定新值 |
示例:
import java.util.concurrent.atomic.AtomicReference;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
//原子更新引用类型演示
referenceDemo();
}
private static void referenceDemo() {
System.out.println("原子更新引用演示:");
AtomicReference<Coupon> ref = new AtomicReference<>();
Coupon kfc = new Coupon("Vivo50", 50);
Coupon mcDonoald = new Coupon("Wheat Gate", 45);
ref.set(mcDonoald);
ref.compareAndSet(mcDonoald, kfc);
System.out.println("优惠券:" + ref.get().getTitle() + ", 余量:" + ref.get().getRemains());
}
static class Coupon {
private String title;
private int remains;
public Coupon(String title, int remains) {
this.title = title;
this.remains = remains;
}
}
}
运行结果:
原子更新引用演示:
优惠券:Vivo50, 余量:50
结构:
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = 1848883965231344442L;
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInIntializerError(e);
}
}
// 方法...
}
public class AtomicMarkableReference<V> {
private static class Pair<T> {
final T reference;
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}
private volatile Pair<V> pair;
public AtomicMarkableReference(V initialRef, boolean initialMark) {
pair = Pair.of(initialRef, initialMark):
}
// 方法...
}
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair<T>(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<T> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
// 方法...
}
5-6.4 原子更新数组类型
原子更新数组类型更新的是数组当中的元素。原子更新数组类型也有三种:AtomicIntegerArray
, AtomicLongArray
以及 AtomicReferenceArray
。
构造方法:
构造方法 | 描述 |
---|---|
AtomicIntegerArray(int length) AtomicIntegerArray(int[] array) |
创建一个具有指定长度,元素初始为 0 的原子整型数组 创建一个与指定数组长度、元素一致的原子整型数组 |
AtomicLongArray(int length) AtomicLongArray(long[] array) |
创建一个具有指定长度,元素初始为 0 的原子长整型数组 创建一个与指定数组长度、元素一致的原子长整型数组 |
AtomicReferenceArray(int length) AtomicReferenceArray(E[] array) |
创建一个具有指定长度,元素初始为空的原子引用数组 创建一个与指定数组长度、元素一致的原子引用数组 |
常用方法:
常用方法 | 描述 |
---|---|
int/long/E get(int i) |
返回数组中指定索引处的元素 |
int/long/E getAndSet(int i, int/long/E newValue) |
原子地将数组指定索引处的元素更新为指定新值,并返回旧值 |
boolean compareAndSet(int i, int/long/E expectedValue, int/long/E newValue) |
当元素当前值与期望值相等时,将元素值修改为指定新值 |
int length() |
返回数组长度 |
int/long addAndGet(int i, int/long delta) int/long decrementAndGet(int i) int/long getAndAdd(int i, int/long delta) int/long getAndDecrement(int i) int/long getAndIncrement(int i) int/long IncrementAndGet(int i) |
语义同原子更新基本类型 |
结构:以 AtomicIntegerArray
为例。
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
private static final VarHandle AA = MethodHandles.arrayElementVarHandle(int[].class);
private final int[] array;
public final void set(int i, int newValue) {
AA.setVolatile(array, i, newValue);
}
public final int getAndSet(int i, int newValue) {
return (int) AA.getAndSet(array, i, newValue);
}
public final boolean compareAndSet(int i, int expectedValue, int newValue) {
return AA.compareAndSet(array, i, expectedValue, newValue);
}
// 方法...
}
AtomicIntegerArray
内部维护了一个 final
修饰的 int
数组,类中所有操作都是针对数组元素的原子操作,保证了线程安全。
5-6.5 原子更新对象属性
若只想更新某个对象中的某个字段,可以使用对象字段的原子类。有三个类:AtomicIntegerFieldUpdater
, AtomicLongFieldUpdater
, AtomicReferenceUpdater
。
使用条件:
- 被操作的字段不能是静态的(
static
); - 被操作的字段不能是最终的(
final
); - 被操作的字段必须由
volatile
修饰; - 属性必须对于当前的更新器所在区域可见;
构造方法:该类的构造方法声明为 protected
,但提供了静态的工厂方法,返回一个该类的对象。
静态方法 | 描述 |
---|---|
AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) |
创建并返回一个关联对象指定字段的更新器 |
AtomicLongFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) |
创建并返回一个关联对象指定字段的更新器 |
AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass, Class<W> wclass, String fieldName) |
创建并返回一个关联对象指定字段的更新器 |
示例:以上文的 Coupon
为例。
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicDemo {
public static void main(String[] args) {
//更新器演示
updaterDemo();
}
private static void updaterDemo() {
Coupon kfc = new Coupon("Vivo50", 50);
Coupon mcDonoald = new Coupon("Wheat Gate", 50);
AtomicIntegerFieldUpdater<Coupon> updater = AtomicIntegerFieldUpdater.newUpdater(Coupon.class, "remains");
//利用更新器更新字段
updater.set(kfc, 100);
System.out.println("使用更新器修改对象字段,修改后:");
System.out.println("KFC: " + kfc.getRemains() + ", MCD: " + mcDonoald.getRemains());
}
static class Coupon {
private String title;
private volatile int remains;
public Coupon(String title, int remains) {
this.title = title;
this.remains = remains;
}
//...
}
}
运行结果:
使用更新器修改对象字段,修改后:
KFC: 100, MCD: 50
5-6.6 CAS 算法与自旋
文首提到,原子包中所提供的类是免锁的线程安全类,而无锁的实现依赖于 CAS 算法。
5-6.6.1 CAS 算法介绍
无锁核心:自旋 + CAS 算法。
CAS 算法:CAS 全称为 Compare and Swap,比较与交换。CAS 机制主要用于一个变量(操作)进行原子性的操作。
CAS 的表达式为 compareAndSet(V,E,N)
。其中,V
表示要更新的变量,E
表示期望值,N
表示修改值。调用该函数修改变量 V
时,会先查看 V
的值是否符合期望值 E
。若符合,则会将该值更新为 E
;否则,V
的值不符合期望值,说明有其他线程正在修改该变量,则不更新变量,而是重新读取变量的值再次调用该函数尝试更新变量,直至修改成功。这个重新读取再尝试的循环过程称为自旋。
自旋锁:自旋锁是一种轻量级的锁机制,用于解决对某项资源的互斥使用。在任何时刻,自旋锁最多只能有一个持有者。一旦该锁由某一线程持有,其他线程尝试获取该锁时,会一直循环(忙等待)检测锁是否被释放,线程不会被阻塞(non-blocking),而不会进入线程挂起或睡眠状态,带来一定程度上的消耗。
自旋锁的原理较为简单,若持有者县城能够在短时间内释放锁,那么这些等待锁的竞争线程不需要进入阻塞状态,只需要等一等(自旋)即可。但是,长时间不释放锁的话,自旋锁将会十分消耗性能,因为这时可能有大量的线程在自旋,占用 CPU 资源。
CAS 缺点:CAS 可以实现无锁状态下的同步,但也有一些局限性。
- 只能保证一个共享变量的原子性:CAS 不如监视器锁和可重入锁一样可保证一段代码和多个变量的同步,要想实现多变量同步,还是需要加锁处理;
- 存在性能开销问题:由于 CAS 需要自旋操作,若长时间自旋都无法成功修改变量,会给 CPU 带来很大的开销;
- ABA 问题:CAS 是通过检查变量值是否发生改变从而保证原子性。设存在两个线程,线程 1 和 2 同时都读到了变量值为 A,但此时线程 1 将变量的值改为了 B,又从 B 改回了 A,期间线程 2 一直没有抢到 CPU 的执行权,直到线程 1 将变量改回原值后才得以执行。这时,线程 2 就不清楚该变量的值曾变过,发生了 ABA 问题。ABA 的问题处理方法比较简单,只需要在更新值的同时更新版本号即可,
AtomicStampedReference
就是通过这种方式解决这一问题的。
CPU 对 CAS 的支持:在操作系统中 CAS 操作实际上是一条 CPU 的原子指令,在操作系统中属于一种原语,原语由多条指令组成,其执行是连续不可中断的。CAS 操作看上去是一个先比较后交换的操作,但实际上是由 CPU 保证了原子操作。
5-6.6.4 从源码角度看 CAS
源码解析:这里以 AtomicInteger
为例,看看 CAS 是如何工作的。
这里,我们主要来看看 incrementAndGet
方法的源码。
//incrementAndGet 源码
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
方法通过一个变量 U
,调用了其对应类中的方法。追踪该变量,可以看到,AtomicInteger
中定义了该变量:
private static final Unsafe U = Unsafe.getUnsafe();
其中,Unsafe
是一个执行低级、不安全操作的方法的集合。由于类中大多数方法都是低级的不安全方法,因此只有受信任的代码才能够调用类中的方法。
在处理多线程并发问题时,源码在底层大量地使用了 Unsafe
的 API。
接下来,追踪 Unsafe
的 getAndAddInt
方法:
//return U.getAndAddInt(this, VALUE, 1) + 1;
//offset = VALUE,这实际上是报告具有给定名称字段在其类的存储分配中的位置
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
//自旋锁的循环(自旋)
do {
//原生方法(native),获取旧值
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
//循环体的条件语句处调用方法 weakCompareAndSetInt(Object o, long offset, int expected, int x);
//其底层又调用了 compareAndSetInt(o, offset, expected, x),方法返回值为boolean类型
//其底层所调用的方法是原生方法,若 Java 变量当前拥有期望值(expected),则将变量原子更新为 x
//整个循环使用了CAS算法,o为内存值,v为旧值(期望值),v + delta则是修改后的值
//作用:比较内存中的值与旧值是否相等,是则将修改后的值写入内存,返回true,修改成功;否则不等,无法修改,返回false,修改失败;
//若变量并没有拥有期望值,则继续等待,不断获取旧值,继续自旋
//若变量拥有期望值,则自旋结束
return v;
}
这样来看,Unsafe::getAndAddInt
的语义实际上是在做修改,修改完成后将旧值返回。但再来看 incrementAndGet
方法的返回语句:
return U.getAndAddInt(this, VALUE, 1) + 1;
那么方法最终就会修改并返回更新后的值。
同样地,getAndIncrement
方法在底层也调用了 Unsafe::getAndAddInt
方法,指示其返回语句为:
return U.getAndAddInt(this, VALUE, 1);
5-6.6.5 CAS 与 Unsafe
若细看 AtomicInteger
的源码,所有的操作都是通过 Unsafe
的成员变量实现。这是一个位于 jdk.internal.misc
的类,这个类中提供了大量低执行级别、不安全的操作。虽然该类及其所有方法都是公开的,但应当有限制地使用该类,仅有可信代码才能够获取该类的实例。
获取实例:该类的构造方法私有化,采用单例模式设计,提供了一个静态方法返回其唯一实例。
静态方法 | 描述 |
---|---|
getUnsafe() |
返回 Unsafe 实例,使得调用者能够执行不安全操作。 |
该类的单例模式设计:
public class Unsafe {
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
return theUnsafe;
}
}
Unsafe
中主要的 CAS 算法:
// 都是不可重写的原生方法,参数 o 指的是给定对象,offset 指的是对象内存偏移量,通过该偏移量迅速定位字段
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSetLong(Object o, long offset, long expected, long x);
这些方法都是原生方法,调用底层代码实现。在 JDK 1.8 中还引入了 getAndAddInt
, getAndAddLong
, getAndSetInt
, getAndSetLong
等方法来支持不同类型的 CAS 操作。
除了 CAS 算法,Unsafe
类还提供了线程调度(park
, unpark
)、对象相关(例如绕过构造方法创建对象)、内存操作(例如堆外内存分配)、数组相关(例如定位数组中每个元素在内存中位置)等功能。
5-6.7 乐观锁与悲观锁
乐观锁:从乐观的角度出发,假设每次获取数据时数据都不会被篡改,因此不会上锁。只是在每次修改共享数据时,先检查数据是否被篡改,若没有,则修改数据,若有,则重新获取最新的值。(CAS)
悲观锁:从最坏的角度出发,认为每次获取数据的时候,都有可能被修改。因此每次在操作共享数据前,都会上锁保护数据。(监视器锁)