我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java并发中的ABA问题学习与解决方案

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java并发中的ABA问题学习与解决方案

1.简介

我们将了解在并发编程中的ABA问题。同时学习引起该问题的根因及问题解决办法。

2.Compare and swap

为了理解根本原因,首先回顾一下Compare and swap的概念。Compare and Swap (CAS)在无锁算法中是一种常见的技术。能够保证并发修改共享数据时,一个线程将共享内存修改后,另一线程尝试对共享内存的修改会失败。

我们每次更新时,通过两种信息来实现:要更新的值及原始值。首先Compare and swap 会比较原始值和当前获取到的值。如果相等,那么将值更新为要设置的值。

3. ABA问题

当执行campare and swap会出现失败的情况。例如,一个线程先读取共享内存数据值A,随后因某种原因,线程暂时挂起,同时另一个线程临时将共享内存数据值先改为B,随后又改回为A。随后挂起线程恢复,并通过CAS比较,最终比较结果将会无变化。这样会通过检查,这就是ABA问题。 在CAS比较前会读取原始数据,随后进行原子CAS操作。这个间隙之间由于并发操作,最终可能会带来问题。

3.1 ABA问题的实际场景:账户余额修改

为了通过实例演示ABA问题。我们创建一个银行账户类,该类维护一个整型变量记录账户余额。该类有两个函数:一个用于存钱,一个用于取钱。这些操作使用CAS来修改账户余额。

3.2 账户余额修改时产生的问题

我们来考虑两个线程操作同一个账户时的场景。当线程1取钱时,先读取余额,随后通过CAS操作进行比较。然后,可能由于某些原因,线程1可能发生阻塞。与此同时,线程2同样通过CAS机制,在线程1挂起时,在同一个账户上执行两个操作。首先,改变原始值,这个值已经被线程1在刚才读取。随后线程2又将这个值改为原始值。

一旦线程1恢复后,在线程1看来,没有发生任何变化。cas将会执行成功。

在这里插入图片描述

4.银行取款问题代码演示

创建一个Account类,balance记录账户余额。transactionCount记录成功执行的事务数。currentThreadCASFailureCount来记录CAS操作失败的次数。

接着我们实现一个存款的方法deposit,与取款方法withdraw。为了演示ABA问题,同时实现一个maybeWait方法进行延迟等待。

最终的代码如下:

    public class Account {
        private AtomicInteger balance;
        private AtomicInteger transactionCount;
        private ThreadLocal<Integer> currentThreadCASFailureCount;
        public Account() {
            this.balance = new AtomicInteger(0);
            this.transactionCount = new AtomicInteger(0);
            this.currentThreadCASFailureCount = new ThreadLocal<>();
            this.currentThreadCASFailureCount.set(0);
        }
        public int getBalance() {
            return balance.get();
        }
        public int getTransactionCount() {
            return transactionCount.get();
        }
        public int getCurrentThreadCASFailureCount() {
            return Optional.ofNullable(currentThreadCASFailureCount.get()).orElse(0);
        }
        public boolean withdraw(int amount) {
            int current = getBalance();
            maybeWait();
            boolean result = balance.compareAndSet(current, current - amount);
            if (result) {
                transactionCount.incrementAndGet();
            } else {
                int currentCASFailureCount = currentThreadCASFailureCount.get();
                currentThreadCASFailureCount.set(currentCASFailureCount + 1);
            }
            return result;
        }
        private void maybeWait() {
            if ("thread1".equals(Thread.currentThread().getName())) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        public boolean deposit(int amount) {
            int current = balance.get();
            boolean result = balance.compareAndSet(current, current + amount);
            if (result) {
                transactionCount.incrementAndGet();
            } else {
                int currentCASFailureCount = currentThreadCASFailureCount.get();
                currentThreadCASFailureCount.set(currentCASFailureCount + 1);
            }
            return result;
        }
    }

接着我们对上述代码进行测试。通过maybeWait方法,模拟出现ABA问题。

    @Test
    public void abaProblemTest() throws InterruptedException {
        final int defaultBalance = 50;
        final int amountToWithdrawByThread1 = 20;
        final int amountToWithdrawByThread2 = 10;
        final int amountToDepositByThread2 = 10;
        Assert.assertEquals(0, account.getTransactionCount());
        Assert.assertEquals(0, account.getCurrentThreadCASFailureCount());
        account.deposit(defaultBalance);
        Assert.assertEquals(1, account.getTransactionCount());
        Thread thread1 = new Thread(() -> {
            // this will take longer due to the name of the thread
            Assert.assertTrue(account.withdraw(amountToWithdrawByThread1));
            // thread 1 fails to capture ABA problem
            Assert.assertNotEquals(1, account.getCurrentThreadCASFailureCount());
        }, "thread1");
        Thread thread2 = new Thread(() -> {
            Assert.assertTrue(account.deposit(amountToDepositByThread2));
            Assert.assertEquals(defaultBalance + amountToDepositByThread2, account.getBalance());
            // this will be fast due to the name of the thread
            Assert.assertTrue(account.withdraw(amountToWithdrawByThread2));
            // thread 1 didn't finish yet, so the original value will be in place for it
            Assert.assertEquals(defaultBalance, account.getBalance());
            Assert.assertEquals(0, account.getCurrentThreadCASFailureCount());
        }, "thread2");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        // compareAndSet operation succeeds for thread 1
        Assert.assertEquals(defaultBalance - amountToWithdrawByThread1, account.getBalance());
        //but there are other transactions
        Assert.assertNotEquals(2, account.getTransactionCount());
        // thread 2 did two modifications as well
        Assert.assertEquals(4, account.getTransactionCount());
    }

5.值类型与引用类型的场景

上面的例子中使用了getBalance()方法获取了一个值类型数据。由于使用的是值类型,虽然出现ABA问题,但未对结果造成影响。如果我们操作的是引用类型,那么最终会保存不同的引用对象,会带来意外的结果。

对于引用类型,下面以链栈为例说明。

  • 线程A希望将A结点出栈,此时读取栈顶元素A,准备执行CAS操作,此时由于某种原因阻塞。
  • 线程B开始执行,执行出栈A、B。随后将D、C、A结点压入栈中。
  • 线程A恢复执行。接着执行CAS,比较发现栈顶结点A没有被修改。随后将栈顶结点改为B。由于B线程在第二步时,已经将B结点移除,A线程修改后发生错误。栈的结构发生破坏。

在这里插入图片描述

接着我们通过下面的代码进行演示:

    static class Stack {
        private AtomicReference<Node> top = new AtomicReference<>();
        static class Node {
            String value;
            Node next;
            public Node (String value) {
                this.value = value;
            }
        }
        //出栈
        public Node pop(int time) {
            Node newTop;
            Node oldTop;
            do {
                oldTop = top.get();
                if (oldTop == null) {
                    return null;
                }
                newTop = oldTop.next;
                try {
                    //休眠一段时间,模拟ABA问题
                    TimeUnit.SECONDS.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } while (!top.compareAndSet(oldTop, newTop));
            return oldTop;
        }
        public void push (Node node) {
            Node oldTop;
            do {
                oldTop = top.get();
                node.next = oldTop;
            } while (!top.compareAndSet(oldTop, node));
        }
        public AtomicReference<Node> getTop() {
            return top;
        }
    }
    @Test
    public void testStack() throws Exception{
        Stack stack = new Stack();
        Stack.Node a = new Stack.Node("A");
        Stack.Node b = new Stack.Node("B");
        // 初始化栈结构
        stack.push(b);
        stack.push(a);
        // ABA 测试
        Thread t1 = new Thread(() -> {
            stack.pop(2);
        });
        
        Stack.Node c = new Stack.Node("C");
        Stack.Node d = new Stack.Node("D");
        Thread t2 = new Thread(() -> {
            stack.pop(0);
            stack.pop(0);
            stack.push(d);
            stack.push(c);
            stack.push(a);
        });
        //
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(5);
        Stack.Node top = stack.getTop().get();
        do {
            System.out.println(top.value);
            top = top.next;
        } while (top != null);
    }

6. 解决方法

  • hazard pointer:首先出现问题是因为,多个线程操作共享数据,并未感知到别的线程正在对共享数据进行操作。通过hazard pointer介绍[1],其基本思想就是每个线程维护一个操作列表,在操作一个结点时将其记录。如果一个线程要做结点变更,先搜索线程操作列表,看是否有其它线程操作。如果有则此次操作执行失败。
  • 不变性:从上述栈的例子中可以看到,在对结点A进行比较时,由于A依然是多个线程共享并复用,因此CAS会成功。如果每次操作时,新创建对象而不是复用。这样CAS就会正常提示失败。但这样可能会创建大量对象。

7. Java中的解决方法

Java中提供了两个类来解决这个问题。

  • AtomicStampedReference
  • AtomicMarkableReference

在原有类的基础上,除了比较与修改期待的值外,增加了一个时间戳。对时间戳也进行CAS操作。这也称为双重CAS。从上例中看到。每次修改一个结点,其时间戳都发生变化。这样即使共享一个复用结点,最终CAS也能返回正常的结果。

8. 总结

本文介绍了CAS产生ABA问题的背景,通用解决办法及Java中的解决办法。对于值类型有时发生ABA问题可能并不会造成问题。但对于引用类型,就可能造成歧义,同时破坏数据结构。通过链栈的演示,我们可以有所了解ABA产生的问题。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java并发中的ABA问题学习与解决方案

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java 并发集合中常见的问题与解决方案

Java 并发集合在实际开发中经常遇到的一系列问题,以及对应的解决方案,这些问题包括:集合修改异常、线程安全问题、性能问题、序列化问题等。
Java 并发集合中常见的问题与解决方案
2024-02-07

并发编程中遇到的Python问题及解决方案

标题:并发编程中遇到的Python问题及解决方案引言:在现代计算机系统中,利用并发编程可以充分发挥多核处理器的性能,提高程序的运行效率。Python作为一种广泛使用的编程语言,也具备了强大的并发编程能力。然而,并发编程中常常会遇到一些问题,
2023-10-22

MongoDB技术开发中遇到的并发访问问题解决方案分析

MongoDB技术开发中遇到的并发访问问题解决方案分析引言:在当今互联网时代,数据的规模和复杂性不断增长,使得数据库系统面临着越来越严峻的并发访问问题。尤其在大数据领域,MongoDB作为一款非常受欢迎的NoSQL数据库技术,也面临着并发访
2023-10-22

MySQL中SELECT+UPDATE处理并发更新问题解决方案

这篇文章主要介绍了MySQL中SELECT+UPDATE处理并发更新问题解决方案分享,需要的朋友可以参考下。 问题背景 假设MySQL数据库有一张会员表vip_member(InnoDB表),结构如下:当一个会员想续买会员(只能续买1个月、
2022-05-16

Java多线程之常见锁策略与CAS中的ABA问题怎么解决

本文小编为大家详细介绍“Java多线程之常见锁策略与CAS中的ABA问题怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“Java多线程之常见锁策略与CAS中的ABA问题怎么解决”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一
2023-06-30

Python中的并行编程问题及解决方案

Python中的并行编程问题及解决方案,需要具体代码示例随着多核处理器的普及和计算任务的复杂化,以及数据处理方面的需求增加,利用并行编程可以有效地提高程序的执行效率。Python作为一种高级编程语言,具有简洁、易读、易写的特点,也提供了一些
2023-10-22

PHP开发中常见的隐式转换问题与解决方案

PHP开发中常见的隐式转换问题与解决方案在PHP开发过程中,隐式转换是一个比较容易出现问题的地方。在数据类型转换时,PHP会自动进行一些转换操作,有时候这种隐式转换会导致意想不到的结果。本文将介绍一些常见的隐式转换问题,并给出相应的解决方
PHP开发中常见的隐式转换问题与解决方案
2024-03-08

Go语言项目开发中的常见问题与解决方案

Go语言项目开发中的常见问题与解决方案Go语言作为一种简洁高效的开发语言,受到越来越多的开发者的青睐。在实际的项目开发中,开发者也会遇到一些常见的问题。本文将提供一些常见问题的解决方案,帮助开发者更好地应对挑战。一、依赖管理在Go语言项目中
Go语言项目开发中的常见问题与解决方案
2023-11-04

事件冒泡引发的常见问题与解决方案

事件冒泡(event bubbling)是指在DOM中,当一个元素上的事件被触发时,它会向上冒泡到该元素的父级元素,再向上冒泡到更高级别的父级元素,直至冒泡到文档的根节点。虽然事件冒泡在许多情况下非常有用,但有时它也会引发一些常见的问题。本
事件冒泡引发的常见问题与解决方案
2024-02-22

Java多线程编程中的并发安全问题及解决方法

保障多线程并发安全,解决线程同步与锁竞争问题,提高应用性能与可靠性。多线程编程需要考虑线程安全性,使用同步机制保证共享变量的一致性,避免线程竞争导致的数据不一致与死锁等问题。常用的同步机制包括synchronized、ReentrantLock、volatile等
2023-05-16

利用MongoDB技术开发中遇到的并发控制问题的解决方案探究

利用MongoDB技术开发中遇到的并发控制问题的解决方案探究摘要:随着互联网技术的快速发展,数据量的不断增大和用户数的不断增加,对于大型应用程序而言,并发控制变得愈发重要。并发控制问题是指在多个用户同时对同一个数据进行读写操作时,可能导致数
2023-10-22

C++中堆和栈问题的分析与解决方案

C++中堆和栈问题的分析与解决方案在C++编程中,堆和栈是两种常用的内存管理方式。堆用于动态分配内存,而栈则用于存储局部变量和函数调用的上下文信息。然而,错误的使用堆和栈可能导致内存泄漏、段错误和无法预料的行为。因此,在编写C++代码时需要
2023-10-22

C++中异常安全性的问题与解决方案

C++中异常安全性的问题与解决方案引言:在C++编程过程中,异常处理是一个重要的方面。异常的发生可能会导致程序的崩溃,严重影响程序的稳定性和可靠性。因此,异常安全性是一个需要重视的问题。本文将探讨在C++中的异常安全性问题,并提供解决方案,
2023-10-22

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录