只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

41、原子类

内容来自王争 Java 编程之美

上一节我们讲到多线程开发中的基础中的基础:CAS,自旋 + CAS 可以替代锁,用于资源竞争不激烈的场景
不过相对于锁来说,自旋 + CAS 的代码实现比较复杂,我们需要先创建 Unsafe 对象,然后获取待更新变量的偏移位置,最后调用 Unsafe 对象的 CAS 方法来更新变量
为了方便开发,JUC 提供了各种原子类,封装了对各种类型数据的自旋 + CAS 操作,这样我们拿来直接使用即可
本节我们就来讲解一下这些原子类,同时借此加深对自旋 + CAS 的理解

1、原子类概述

原子类主要依赖自旋 + CAS 来实现,原子类中的每个操作都是原子操作,在多线程环境下,执行原子类中的操作不会出现线程安全问题
根据处理的数据类型,原子类可以粗略地分为 4 类:基本类型原子类、引用类型原子类、数组类型原子类、对象属性原子类
每类原子类包含的具体类如下图所示
image

对于以上四种原子类,我们重点讲解比较常用的:基本类型原子类、引用类型原子类
对于:数组类型原子类、对象属性原子类,我们只做简单介绍

2、基本类型原子类

JUC 提供了 3 个基本类型原子类,它们分别是:AtomicBoolean、AtomicInteger、AtomicLong
因为浮点数无法精确表示和比较大小,所以 JUC 并没有提供浮点类型的原子类
除此之外,对于 char 基本类型,我们需要将其转化为为 int 类型,进而使用 AtomicInteger 来进行原子操作

AtomicBoolean、AtomicInteger、AtomicLong 这 3 个基本类型原子类的使用方法和实现原理非常相似,我们拿 AtomicInteger 举例讲解
AtomicInteger 类的部分源码如下所示,AtomicInteger 类中包含的核心的原子操作暂时未给出,待会一一讲解

public class AtomicInteger {
private volatile int value;
// 创建 Unsafe 对象, 获取 value 变量在 AtomicInteger 对象中的偏移位置
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
// 构造函数
public AtomicInteger(int initialValue) {
value = initialValue;
}
// 默认 value 值为 0
public AtomicInteger() {
}
// 基本的 getter、setter 方法
public final int get() {
return value;
}
public final void set(int newValue) {
value = newValue;
}
// ... 省略核心原子操作 ...
}

接下来,我们重点讲解一下 AtomicInteger 提供的 4 类核心原子操作:CAS、增加、自增、自减

2.1、CAS 函数

AtomicInteger 中的 compareAndSet() 是标准的 CAS 函数
如下代码所示,如果 value 值等于入参 expect 值,那么就将 value 值更新为入参 update 值,并返回 true
compareAndSet() 函数底层调用 Unsafe 类的 CAS 方法 compareAndSwapInt() 来实现,此方法在上一节中已经详细讲解,这里就不再赘述了

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

2.2、增加函数

增加函数有两个,从命名上我们也可以大概猜出两者的不同之处

  • getAndAdd() 函数先获取 value 值,再更新 value,函数返回更新之前的旧值
  • addAndGet() 函数先更新 value 值,再获取 value,函数返回更新之后的新值
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

两个函数的代码实现非常相似,addAndGet() 函数只不过是在 getAndAdd() 返回值的基础之上,加了一个 delta 再返回而已,因此我们重点看下 getAndAdd() 函数
getAndAdd() 函数调用 Unsafe 类的 getAndAddInt()方法来实现,getAndAddInt() 方法的代码实现如下所示

public final int getAndAddInt(Object o, long offset, int delta) {
int oldValue;
// 自旋 + CAS
do {
oldValue = this.getIntVolatile(o, offset); // 调用 Unsafe 类的 native 方法
} while(!this.compareAndSwapInt(o, offset, oldValue, oldValue + delta));
return oldValue;
}

getAndAddInt() 并非 native 方法,而是直接由 Java 代码实现,其底层调用 Unsafe 类的 CAS 方法 compareAndSwapInt() 来实现
在多线程竞争执行时,CAS 有可能执行失败,因此 getAndAddInt() 采用自旋重复执行 CAS,直到成功为止,这样就可以保证 getAndAddInt() 总是可以将 value 值增加 delta

2.3、自增函数

自增函数也有两个,用法跟增加函数类似

  • getAndIncrement() 函数返回自增之前的旧值
  • incrementAndGet() 函数返回自增之后的新值

底层实现原理也跟增加函数类似,只需要将增加函数中的 delta 设置为 1 即可

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

2.4、自减函数

自减函数也有两个,用法跟自增函数类似

  • getAndDecrement() 函数返回自减之前的旧值
  • decrementAndGet() 函数返回自减之后的新值

底层实现原理跟增加函数类似,只需要将增加函数中的 delta 设置为 -1 即可

public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

从功能上,AtomicInteger 提供的增加函数,完全可以替代自增函数和自减函数
AtomicInteger 类提供冗余的自增函数和自减函数,完全是为了方便程序员使用

3、引用类型原子类

基本类型原子类提供了操作基本类型数据的原子函数,引用类型原子类提供了操作引用类型数据的原子函数
JUC 提供了 3 个引用类型原子类,它们分别是:AtomicReference、AtomicStampedReference、AtomicMarkableReference
接下来我们一一详细讲解这 3 个引用类型原子类的用法和实现原理

3.1、AtomicReference

AtomicReference 类的部分源码如下所示
基本类型原子类主要依赖 Unsafe 类中的 compareAndSwapInt() 和 compareAndSwapLong() 这两个 CAS 方法来实现
引用类型原子类主要依赖 Unsafe 类中的 compareAndSwapObject() 这个 CAS 方法来实现,AtomicReference 的实现方式跟 AtomicInteger 类似,这里就不再赘述了

public class AtomicReference<V> {
private volatile V value;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
public final V get() {
return value;
}
public final void set(V newValue) {
value = newValue;
}
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
}

AtomicReference 类的使用方式,如下示例代码所示

public class DemoLock {
private AtomicReference<Thread> owner = new AtomicReference<>(null);
public boolean tryAcquire() {
return owner.compareAndSet(null, Thread.currentThread());
}
}

3.2、AtomicStampedReference

相对于 AtomicReference,AtomicStampedReference 增加了版本戳,主要是用来解决 CAS 的 ABA 问题,什么是 CAS 的 ABA 问题呢?我们举例解释一下
如下代码所示,addAtHead() 函数往链表头部添加节点,removeAtHead() 函数从链表头部移除节点,你觉得下面的代码是否是线程安全的呢?

public class Node {
private char val;
private Node next;
public Node(int val, Node next) {
this.val = val;
this.next = next;
}
}
public class LinkedList {
private Node head = null;
public void addAtHead(Node newNode) {
newNode.next = head;
head = newNode;
}
public void removeAtHead() {
if (head != null) {
head = head.next;
// 上述语句相当于以下两个语句
// Node tmp = head.next;
// head = tmp;
}
}
}

实际上 LinkedList 是非线程安全的,我们分 3 种情况来分析一下

问题一

两个线程竞争交叉执行 addAtHead() 函数,有可能会导致节点无法正常添加,如下图所示
image

问题二

两个线程竞争交叉执行 removeAtHead() 函数,有可能会导致 NullPointerException 异常,或者节点无法正常移除,如下图所示
image

问题三

两个线程竞争交叉执行 addAtHead() 函数和 removeAtHead() 函数,有可能导致节点无法正常移除,或者节点无法正常添加,如下图所示
image

解决一

实际上不管是 addAtHead(),还是 removeAtHead(),线程不安全的本质原因是:在更新 head 时,head 有可能已经被其他线程更改
为了解决 LinkedList 的线程安全问题,我们既可以基于 synchronized 或 Lock 锁来解决,也可以基于自旋 + CAS 来解决
基于锁的解决方案比较简单,这里就留给你自己思考
我们重点来看下基于自旋 + CAS 的解决方案,代码实现如下所示,在更新 head 时,我们通过 CAS 确保 head 没有被其他线程更新过

public class LinkedListThreadSafe {
private AtomicReference<Node> head = new AtomicReference<>(null);
public void addAtHead(Node newNode) {
boolean succeed = false;
while (!succeed) {
Node oldHead = head.get();
newNode.next = oldHead;
succeed = head.compareAndSet(oldHead, newNode);
}
}
public void removeAtHead() {
boolean succeed = false;
while (!succeed) {
Node oldHead = head.get();
if (oldHead == null) return;
Node nextNode = oldHead.next;
succeed = head.compareAndSet(oldHead, nextNode);
}
}
}

不过以上代码实现仍然存在问题,如下图所示
线程 1 执行了一次 removeAtHead() 函数,线程 2 执行了两次 removeAtHead() 函数和一次 addAtHead() 函数
因此链表中的节点个数最终应该是 1,但实际的运行结果却是 2,这是为什么呢?
image

实际上引起执行结果出错的原因,就是 CAS 的 ABA 问题

  • 线程 1 执行 removeAtHead() 函数,设置 oldHead = A、nextNode = B
  • 在执行 CAS 之前,线程 2 将 head 从 A 变为 B、C,最后又变为 A,尽管线程 2 执行完之后,head 仍为 A,但链表的整体结构发生了变化
  • 随后当线程 1 执行 CAS 时,检查当前的 head 跟 oldHead 相等,仍然是 A,错以为期间没有其他线程执行 addAtHead() 和 removeAtHead() 函数,于是成功执行 CAS

解决二

为了解决以上 ABA 问题,我们使用 AtomicStampedReference 对 LinkedList 进行改造,如下代码所示

public class LinkedList {
private AtomicStampedReference<Node> head = new AtomicStampedReference<>(null, 0); // stamp 初始值为 0
public void addAtHead(Node newNode) {
boolean succeed = false;
while (!succeed) {
int oldStamp = head.getStamp();
Node oldHead = head.getReference();
newNode.next = oldHead;
succeed = head.compareAndSet(oldHead, newNode, oldStamp, oldStamp + 1);
}
}
public void removeAtHead() {
boolean succeed = false;
while (!succeed) {
int oldStamp = head.getStamp();
Node oldHead = head.getReference();
if (oldHead == null) return;
Node nextNode = oldHead.next;
succeed = head.compareAndSet(oldHead, nextNode, oldStamp, oldStamp + 1);
}
}
}

原理

AtomicStampedReference 的部分源码如下所示,相对于 AtomicReference,AtomicStampedReference 增加了一个 int 类型 stamp 版本戳
它将 stamp 和引用对象 reference(例如 head)封装成一个新的 Pair 对象,在 Pair 对象上执行 CAS
即便引用对象存在 ABA 问题,但是 stamp 总是在增加,stamp 不会存在 ABA 问题,因此两者组合而成的 Pair 对象,也不就不存在 ABA 问题了

public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(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<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return unsafe.compareAndSwapObject(this, pairOffset, cmp, val);
}
}

3.3、AtomicMarkableReference

AtomicMarkableReference 跟 AtomicStampedReference 的作用相同,也是用来解决 AtomicReference 存在的 ABA 问题,区别在于

  • AtomicStampedReference 使用 int 类型的 stamp 版本戳是否改变,来判断 reference 是否有被更改过
  • AtomicMarkableReference 使用 boolean 类型的 mark 是否改变(true 变成 false 或 false 变成 true),来判断 reference 是否有被更改过

AtomicMarkableReference 的用法和实现原理跟 AtomicStampedReference 非常相似,这里就不再赘述了

4、数组类型原子类

JUC 提供了 3 个数组类型原子类,它们分别是:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
这 3 个类的使用方式和实现原理非常类似,我们拿 AtomicIntegerArray 举例讲解

实际上 AtomicIntegerArray 中的原子操作,跟 AtomicInteger 中的原子操作一一对应,只是在操作中多了一个下标而已
我们拿 CAS 操作来举例,两个类中的 CAS 函数对比如下所示
AtomicIntegerArray 中的 CAS 函数,也是用来 Unsafe 中的 compareAndSwapInt() 来实现的,只不过计算元素的偏移位置比较复杂而已

// AtomicInteger
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// AtomicIntegerArray
public final boolean compareAndSet(int i, int expect, int update) {
return unsafe.compareAndSwapInt(array, checkedByteOffset(i), expect, update);
}

5、对象属性原子类

JUC 提供了 3 个对象属性原子类,它们分别是:AtomicIntegerFiledUpdater、AtomicLongFieldUpdater、AtomicReferenceFiledUpdater
如果某个类中的属性没有提供合适的原子操作,那么我们可以使用对象属性原子类来对其进行原子操作,不过允许这样做的前提是:属性必须是 public 的

因为对象属性原子类很少用到,所以我们仅仅举例简单介绍一下,不做深入分析

public class Updater {
private static AtomicIntegerFieldUpdater<Node> updater = AtomicIntegerFieldUpdater.newUpdater(Node.class, "val");
public static void incrementVal(Node node) {
updater.incrementAndGet(node);
}
}
public class AtomicIntegerFieldUpdaterExample {
private static class Data {
public volatile int value;
}
public static void main(String[] args) {
AtomicIntegerFieldUpdater<Data> updater = AtomicIntegerFieldUpdater.newUpdater(Data.class, "value");
Data data = new Data();
data.value = 10;
// 原子地增加 value 的值
updater.incrementAndGet(data);
System.out.println("Incremented value: " + data.value);
// 原子地比较并设置 value 的值
updater.compareAndSet(data, 11, 20);
System.out.println("Updated value: " + data.value);
}
}

6、课后思考题

对于 AtomicInteger 类中的 getAndAdd() 函数,如果我们不使用 CAS 来实现,如下代码实现方式,是否是线程安全的?为什么呢?

public final int getAndAdd(int delta) {
int oldValue = value;
value += delta;
return oldValue;
}

不是线程安全的,因为 value += delta 是类似自增的非原子操作,所以多线程并发执行这条语句会存在线程安全问题

posted @   lidongdongdong~  阅读(45)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开