前言
第一章还没看过?点我可以穿越
阅读本篇文章,你需要了解以下知识:
从上一章的内容,我们可以了解到,Atomic
可以基本解决线程同步安全的问题。而本章我们将讨论Atomic
的缺点与它的原子性。
ABA问题
什么是ABA问题
?首先我们都知道,Atomic
的CAS
模型,会先读取变量的值,作为预期旧值,然后再基于旧值产生操作生成新值,再确认变量是否为预期旧值,如果是,修改为新值。
我们以单向链表来演示ABA
会导致的问题:
解决ABA问题
现在我们知道了,由于Atomic
仅判断了旧值
,但并没有意识到整个链表已经被修改过一次了。所以我们要引入一个新的概念:
版本
Atomic
在修改值时,保存的不仅再是旧值,还有一个版本号。在每次更改后,版本号都会变化,这样就不会再产生ABA问题了。我们看图:
AtomicStampedReference
Atomic
的开发者自然也意识到了这个问题,并后续开发了AtomicStampedReference
来修复这个问题。我们用一段简单的代码来实现:
1import java.util.concurrent.atomic.AtomicStampedReference;
2
3public class Main {
4 public static void main(String[] args) {
5 /*
6 实例化有版本标记的Atomic类
7 传参1:初始化版本
8 传参2:初始化值
9 */
10 AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 66);
11 /*
12 打印值
13 getStamp()方法获取当前值
14 */
15 System.out.println("当前值:" + atomicStampedReference.getStamp() + " 当前版本:" + atomicStampedReference.getReference());
16 /*
17 使用compareAndSet(V expectedReference, V newReference, int expectedStamp,mint newStamp)方法修改值
18 传参1:预期中的版本
19 传参2:如果修改时预期中的版本和旧值正确,则修改为指定版本
20 传参3:预期中的旧值
21 传参4:如果修改时预期中的版本和旧值正确,则修改为指定值
22 */
23 System.out.println(
24 atomicStampedReference.compareAndSet(
25 1,
26 2,
27 atomicStampedReference.getStamp(),
28 atomicStampedReference.getStamp() + 22
29 )
30 );
31 //再次打印值
32 System.out.println("当前值:" + atomicStampedReference.getStamp() + " 当前版本:" + atomicStampedReference.getReference());
33 }
34}
得到结果:
1当前值:66 当前版本:1
2true
3当前值:88 当前版本:2
实现源码(选读)
让我们来看看,我们用来修改值的compareAndSet()
方法是如何实现的:
默认构造方法
1 /**
2 * Creates a new {@code AtomicStampedReference} with the given
3 * initial values.
4 *
5 * @param initialRef the initial reference
6 * @param initialStamp the initial stamp
7 */
8 public AtomicStampedReference(V initialRef, int initialStamp) {
9 //生成新的集合并存储
10 pair = Pair.of(initialRef, initialStamp);
11 }
当我们实例化AtomicStampedReference
时,这段代码会执行。Pair
是一个集合,用于存储预期值
和预期版本
。
compareAndSet
1 /**
2 * Atomically sets the value of both the reference and stamp
3 * to the given update values if the
4 * current reference is {@code ==} to the expected reference
5 * and the current stamp is equal to the expected stamp.
6 *
7 * @param expectedReference the expected value of the reference
8 * @param newReference the new value for the reference
9 * @param expectedStamp the expected value of the stamp
10 * @param newStamp the new value for the stamp
11 * @return {@code true} if successful
12 */
13 public boolean compareAndSet(V expectedReference,
14 V newReference,
15 int expectedStamp,
16 int newStamp) {
17 //生成一个新的集合,用于和存储的集合对比
18 Pair<V> current = pair;
19 return
20 expectedReference == current.reference &&
21 expectedStamp == current.stamp &&
22 //短路与,如果上方存储的预期值相等,则执行下方内容(赋予新值和新版本),并返回true
23 ((newReference == current.reference &&
24 newStamp == current.stamp) ||
25 //如果修改失败,则使用CAS
26 casPair(current, Pair.of(newReference, newStamp)));
27 }
后语
自JDK5
版本开始,新增了AtomicStampedReference
,它能利用版本戳很好地解决ABA问题
。
但相对的,AtomicStampedReference
可能会对内存空间和性能产生一些小的影响,当大量线程访问相同的原子值时,性能会大幅下降。所以JDK8
增加了LongAdder
和LongAccumulator
类以解决这个问题。
至于Atomic
拥有原子性
,原因是Atomic
修改值的过程非常严谨,不会被打断,所以总能得到预期的值。