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

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

什么是ABA问题?

在 Java 并发编程中,多个线程同时访问同一个共享变量时,由于线程调度不确定性,可能导致读写出现交叉,进而出现意料之外的问题。其中比较典型的就是 ABA 问题。

ABA 问题的简介来说,就是:线程1将共享变量A的值由原来的值A1修改为A2,然后又将A2修改为A1;这时线程2也来操作变量A,判断变量A的值仍然为A1,认为之前的操作未产生影响,然后执行其他操作。但是这个操作过程中忽略了 A 变量的第二次修改,这时候可能会造成意料之外的结果。

如何理解ABA问题?

ABA 问题与你跟好友到网吧玩 CS 或 PUBG 过程中非常相似。假设你开头去了洗手间,好友在那里等你进场。你误认为他不在,随意找了人一起玩起了 CS/PUBG,然后第三把结束之后又去洗手间了,这时你的好友回到了你们座位旁,看到你不见了,然后你回来时他还在这里,于是你们继续愉快的玩游戏。这种情况就是 ABA 问题:A 变量的状态 1 -> 2 -> 1,而你(线程 2)在看到状态为 1 的时候不知道它还经历了状态 2。

ABA问题的解决方案

方法一:使用AtomicStampedReference

解决这个问题的主要方案是使用 AtomicStampedReference 类,它通过使用版本戳(Stamp)的机制来解决这个问题。修改过程中,除了记录变量的值外,还会记录一个版本号,所以就不仅仅修改一个值了。

public class ABAProblem {
    private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(1, 1);

    public static void solveABAProblem() {
        new Thread(() -> {
            int stamp = value.getStamp();
            System.out.println(Thread.currentThread().getName() + "第一次获取的stamp:" + stamp);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = value.compareAndSet(1, 10, stamp, ++stamp);
            System.out.println(Thread.currentThread().getName() + "执行CAS操作结果:" + result);
        }, "Thread 1").start();

        new Thread(() -> {
            int stamp = value.getStamp();
            System.out.println(Thread.currentThread().getName() + "第一次获取的stamp:" + stamp);
            boolean flag = true;
            while (flag) {
                stamp = value.getStamp();
                Integer integer = value.getReference();
                if (integer == 1) {
                    if (value.compareAndSet(integer, 5, stamp, ++stamp)) {
                        System.out.println(Thread.currentThread().getName() + "执行CAS操作修改成功" + ",当前stamp:" + stamp + ",修改后的值:" + value.getReference());
                        flag = false;
                    }
                }
            }
        }, "Thread 2").start();
    }

    public static void main(String[] args) {
        solveABAProblem();
    }
}

这段代码中,我们创建了两个线程,第一个线程 Thread 1 将整数变量 value 的初始值设置为1。然后在1500毫秒的间隔之后又将 value 的值从1改成10,以及我们设定的初始版本号1,然后,等待2秒钟,让第二个线程运行起来。线程 Thread 2 首先也获取 value 的初始版本号 1,然后不断进行这个操作:如果 value 的值为 1,则尝试将其修改成 5。

运行后的结果如下所示:

Thread 1第一次获取的stamp:1
Thread 2第一次获取的stamp:1
Thread 2执行CAS操作修改成功,当前stamp:2,修改后的值:5
Thread 1执行CAS操作结果:false

在这段代码中,我们已经解决了 ABA 问题。这是因为我们使用了 AtomicStampedReference,对每次写入值都进行了版本号的更新,如果在另一个线程已经修改过变量时,版本号已经变更,即使写入了相同的值,也会导致 CompareAndSet 失败。这个例子中,CAS 操作成功的线程2,它的操作步骤是先获取当前 AtomicStampedReference 的状态,再进行 CompareAndSet 操作,由于写入值的同时,我们还修改了版本号,所以对应的版本号不同,操作失败。而实际上,此时变量 value 的状态早已经改变,并不是期望的值1了,从而避免了 ABA 问题的发生。

方法二:使用Java 8的StampedLock

Java 8 中新增了一种锁类型 StampedLock,它也可以防止 ABA 问题的出现。

public class StampedLockDemo {
    private static final StampedLock lock = new StampedLock();
    private static int shareValue = 1;

    public static void read() {
        long stamped = lock.tryOptimisticRead();
        int result = shareValue;
        if (!lock.validate(stamped)) { // 读取到的值不一致,需要用悲观读锁重新读取
            stamped = lock.readLock();
            try {
                result = shareValue;
            } finally {
                lock.unlockRead(stamped);
            }
        }
        System.out.println(Thread.currentThread().getName() + ": value=" + shareValue);
    }

    public static void write() {
        long stamped = lock.writeLock();
        try {
            shareValue = shareValue + 1;
        } finally {
            lock.unlockWrite(stamped);
        }
    }

    public static void solveABAProblem() {
        new Thread(() -> {
            int expectedValue = shareValue;
            int newValue = shareValue + 1;
            System.out.println(Thread.currentThread().getName() + "尝试将值从" + expectedValue + "修改为" + newValue);
            write();
            System.out.println(Thread.currentThread().getName() + "将值从" + expectedValue + "改为" + newValue);
        }, "Thread 1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": value=" + shareValue);
            read();
            int expectedValue = shareValue;
            int newValue = shareValue + 1;
            System.out.println(Thread.currentThread().getName() + "尝试将值从" + expectedValue + "修改为" + newValue);
            write();
            System.out.println(Thread.currentThread().getName() + "将值从" + expectedValue + "改为" + newValue);
        }, "Thread 2").start();
    }

    public static void main(String[] args) {
        solveABAProblem();
    }
}

这段代码中,Thread 1Thread 2 分别以 "写" 和 "读" 的方式访问了共享变量 shareValue。为了模拟一个 ABA 问题,Thread 1 先把 shareValue 从初始值 1 改成 2,然后又改回 1。这样的操作其实就是 ABA 问题,而在 Thread 2 中的读操作里,我们使用了 tryOptimisticRead 方法,它可以获取当前的版本戳,并在写操作时不加锁。如果在 Thread 2 执执行过程中写入变量的线程已经修改过值,那么在 validate 方法中会判断版本戳是否一致,如果不一致,则用悲观读锁重新读取变量。

运行结果如下所示:

Thread 1尝试将值从1修改为2
Thread 2: value=1
Thread 2: value=1
Thread 2尝试将值从1修改为2
Thread 2将值从1改为2
Thread 1将值从2改为1
Thread 2尝试将值从2修改为3
Thread 2将值从2改为3

从结果中可以看出,我们已经解决了 ABA 问题。这是因为在读写过程中,通过使用 StampedLock,我们构造出了一个版本戳,并保证了戳码的线性,这样一来,就可以避免发生 ABA 问题。

总结

ABA 问题在 Java 并发编程中常常发生,它能够让开发者感到很困惑。解决 ABA 问题的方式有很多,比如:使用 AtomicStampedReference、使用 Java 8 的 StampedLock、使用 synchronized 等。如果你想了解更多高质量的多线程编程知识,可以参考《Java 并发编程实战》这本书。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java并发中的ABA问题学习与解决方案 - Python技术站

(0)
上一篇 2023年5月17日
下一篇 2023年5月17日

相关文章

  • Java中同步与并发用法分析

    Java中同步与并发用法分析 同步 在Java中,同步是指多个线程之间访问共享资源的时候,保证线程安全的机制。Java提供了两种机制来实现同步:synchronized关键字和Lock接口。 synchronized关键字 synchronized关键字可以用于修饰方法或代码块。被修饰的方法或代码块在同一时间只能被一个线程执行,其他线程需要等待。 示例代码:…

    多线程 2023年5月16日
    00
  • 一文详解如何有效的处理Promise并发

    一文详解如何有效的处理Promise并发 在JavaScript的异步编程中,Promise是一种广泛使用的方式,它能很好地解决回调地狱问题,提高代码的可读性和可维护性。然而,在实际应用中,也会遇到需要同时执行多个Promise的场景,这就需要我们学会如何处理Promise并发。 1. Promise并发的几种基本方式 在处理Promise并发时,主要有以下…

    多线程 2023年5月17日
    00
  • python多线程互斥锁与死锁

    下面是关于“python多线程互斥锁与死锁”的详细讲解。 什么是互斥锁 在多线程编程中,如果多个线程同时对共享资源进行读写操作,可能会导致数据出现混乱或不一致的情况。为了解决这个问题,我们需要使用互斥锁(Mutex)来保证同一时刻只有一个线程访问共享资源。 互斥锁可以分为两种类型:临界区互斥锁和条件变量互斥锁。 临界区互斥锁:在程序中使用一个互斥锁对象来保护…

    多线程 2023年5月16日
    00
  • Java并发编程深入理解之Synchronized的使用及底层原理详解 上

    Java并发编程深入理解之Synchronized的使用及底层原理详解 Synchronized的基本使用 Synchronized是Java中用于实现线程同步的基本方法之一,其使用方式为在方法或代码块前加上synchronized关键词。 public synchronized void method() { // method body } synchr…

    多线程 2023年5月17日
    00
  • Java并发编程之原子操作类详情

    Java并发编程之原子操作类详情 Java中的原子操作类提供了一种线程安全的方式来处理共享变量。它们能够保证多个线程同时修改变量时不会导致数据竞争。 原子操作类的使用 Java中有几个原子操作类,包括AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference。以下是每个类的简短描述: AtomicBoolea…

    多线程 2023年5月17日
    00
  • java并发编程专题(五)—-详解(JUC)ReentrantLock

    Java并发编程专题(五)——详解(JUC)ReentrantLock ReentrantLock是java.util.concurrent(J.U.C)包中的一个锁工具类,也是Java多线程中常用的互斥锁。它可用于代替synchronized关键字进行线程同步,比synchronized更灵活。 1. 使用ReentrantLock 1.1 创建Reent…

    多线程 2023年5月16日
    00
  • Javaweb应用使用限流处理大量的并发请求详解

    Javaweb 应用使用限流处理大量的并发请求详解 在高并发情况下,大量的请求可能会造成服务器的宕机或响应延迟。为了解决这个问题,我们可以使用限流的方法来平滑控制请求的流量和数量。 什么是限流 限流是指在某种情况下控制流量或者节流保持并发线程的数量在合理的范围之内。在实际应用中,限流就是对某种资源或者连接、把它的使用量限制在一定范围内,防止由于某些原因导致的…

    多线程 2023年5月16日
    00
  • C#编程高并发的几种处理方法详解

    C#编程高并发的几种处理方法详解 在C#编程中,高并发的处理是一个非常常见的问题。为了达到良好的并发性能,需要采用一些有效的处理方法。 1. 多线程处理 在高并发情况下,使用多线程处理是一个常见的方法。具体的做法是,将任务分配到多个线程中,每个线程处理一个任务。通过多个线程的并行处理,可以加快任务的处理速度,提高并发性能。在C#中,可以使用Thread类或T…

    多线程 2023年5月16日
    00
合作推广
合作推广
分享本页
返回顶部