大白话之必会Java Atomic | 线程一点也不安全(二):Atomic的ABA问题会导致什么情况?如何解决?

前言

第一章还没看过?点我可以穿越

阅读本篇文章,你需要了解以下知识:

  • Atomic是什么?(点此跳转
  • 单向链表的原理

从上一章的内容,我们可以了解到,Atomic可以基本解决线程同步安全的问题。而本章我们将讨论Atomic的缺点与它的原子性。

ABA问题

什么是ABA问题?首先我们都知道,AtomicCAS模型,会先读取变量的值,作为预期旧值,然后再基于旧值产生操作生成新值,再确认变量是否为预期旧值,如果是,修改为新值。

我们以单向链表来演示ABA会导致的问题:

2.png

解决ABA问题

现在我们知道了,由于Atomic仅判断了旧值,但并没有意识到整个链表已经被修改过一次了。所以我们要引入一个新的概念:

版本

Atomic在修改值时,保存的不仅再是旧值,还有一个版本号。在每次更改后,版本号都会变化,这样就不会再产生ABA问题了。我们看图:

1.png

AtomicStampedReference

Atomic的开发者自然也意识到了这个问题,并后续开发了AtomicStampedReference来修复这个问题。我们用一段简单的代码来实现:

import java.util.concurrent.atomic.AtomicStampedReference;

public class Main {
    public static void main(String[] args) {
        /*
        实例化有版本标记的Atomic类
        传参1:初始化版本
        传参2:初始化值
         */
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 66);
        /*
        打印值
        getStamp()方法获取当前值
         */
        System.out.println("当前值:" + atomicStampedReference.getStamp() + " 当前版本:" + atomicStampedReference.getReference());
        /*
        使用compareAndSet(V expectedReference, V newReference, int expectedStamp,mint newStamp)方法修改值
        传参1:预期中的版本
        传参2:如果修改时预期中的版本和旧值正确,则修改为指定版本
        传参3:预期中的旧值
        传参4:如果修改时预期中的版本和旧值正确,则修改为指定值
         */
        System.out.println(
                atomicStampedReference.compareAndSet(
                1,
                2,
                atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp() + 22
                 )
        );
        //再次打印值
        System.out.println("当前值:" + atomicStampedReference.getStamp() + " 当前版本:" + atomicStampedReference.getReference());
    }
}

得到结果:

当前值:66 当前版本:1
true
当前值:88 当前版本:2

实现源码(选读)

让我们来看看,我们用来修改值的compareAndSet()方法是如何实现的:

默认构造方法

    /**
     * Creates a new {@code AtomicStampedReference} with the given
     * initial values.
     *
     * @param initialRef the initial reference
     * @param initialStamp the initial stamp
     */
    public AtomicStampedReference(V initialRef, int initialStamp) {
	//生成新的集合并存储
        pair = Pair.of(initialRef, initialStamp);
    }

当我们实例化AtomicStampedReference时,这段代码会执行。Pair是一个集合,用于存储预期值预期版本

compareAndSet

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
	//生成一个新的集合,用于和存储的集合对比
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            //短路与,如果上方存储的预期值相等,则执行下方内容(赋予新值和新版本),并返回true
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
		//如果修改失败,则使用CAS
             casPair(current, Pair.of(newReference, newStamp)));
    }

后语

JDK5版本开始,新增了AtomicStampedReference,它能利用版本戳很好地解决ABA问题

但相对的,AtomicStampedReference可能会对内存空间和性能产生一些小的影响,当大量线程访问相同的原子值时,性能会大幅下降。所以JDK8增加了LongAdderLongAccumulator类以解决这个问题。

至于Atomic拥有原子性,原因是Atomic修改值的过程非常严谨,不会被打断,所以总能得到预期的值。

如转载请在文章尾部添加

原作者来自 adlered 个人技术博客:https://www.stackoverflow.wiki/

评论

  1. 共同进步😄

  2. 学习了👍

取消