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

本贴最后更新于 2052 天前,其中的信息可能已经时移世改

前言

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

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

  • 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 修改值的过程非常严谨,不会被打断,所以总能得到预期的值。

  • 大白话
    17 引用 • 27 回帖
  • 原理
    16 引用 • 44 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3198 引用 • 8215 回帖 • 1 关注
  • 线程
    123 引用 • 111 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...