【CAS、ABA、ABA 解决之 AtomicStampedReference】

介绍

CAS 表示比较并交换(Compare and Swap),ABA 表示原子化的 ABA 问题,即在多线程环境下,当一个值在操作之前和之后都没有发生变化,但是期间发生了一些其他的变化,导致出现了 ABA 问题。为了解决这些问题,Java 提供了 Atomic 类型和 AtomicReference 类型。

Atomic 类型提供了对基本数据类型的原子化操作,例如 AtomicInteger 和 AtomicLong 类型可以对 int 和 long 类型进行原子化的加、减、乘、除等操作。

AtomicReference 类型提供了对对象类型的原子化操作,例如 AtomicReference 类型可以对任意对象进行原子化的设置、获取、比较和交换操作。

CAS

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用了 AtomicInteger 类型,先将其初始化为 2022,然后使用 compareAndSet 方法进行原子化的比较和交换操作,
 * 将其值从 2022 修改为 2023。输出结果为 true 2023,说明原子化的比较和交换操作执行成功。
 * @author Administrator
 */
public class MyCAS {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2022);
        boolean result = atomicInteger.compareAndSet(2022, 2023);
        System.out.println(result + "\t" + atomicInteger.get());
    }
}

ABA

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 有两个线程 t1 和 t2。线程 t1 通过两次 CAS 操作将原子引用的值先改为 2023,再改回 2022,
 * 模拟了 ABA 问题的发生。线程 t2 在线程 t1 执行完成后,先获取原子引用的当前值和版本号,模拟了一些其他的操作,
 * 然后尝试进行 CAS 操作,将值从当前值加 999。由于线程 t2 并没有使用带有版本号的 CAS 操作,
 * 因此它无法识别出 ABA 问题的存在,从而导致 CAS 操作成功,输出结果为 true 3021。
 * @author Administrator
 */
public class MyABA {
    static AtomicInteger atomicInteger = new AtomicInteger(2022);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            atomicInteger.compareAndSet(2022, 2023);
            atomicInteger.compareAndSet(2023, 2022);
            System.out.println(Thread.currentThread().getName() + "\t已经完成一次 ABA 操作");
        }, "t1").start();

        // 保证 t1 先执行完一次 ABA 操作
        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            // 获取当前的值和版本号
            int expect = atomicInteger.get();

            // 模拟进行一次其他的操作
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 尝试进行 CAS 操作
            boolean result = atomicInteger.compareAndSet(expect, expect + 999);
            System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + result + "\t当前实际值:" + atomicInteger.get());
        }, "t2").start();

        TimeUnit.SECONDS.sleep(3);

        System.out.println("最终结果:" + atomicInteger.get());
    }
}

ABA 解决

/**
 * 有两个线程 t1 和 t2。线程 t1 使用带有版本号的 CAS 操作将原子引用的值先改为 2023,再改为 2022。
 * 线程 t2 在线程 t1 执行完成后,使用带有版本号的 CAS 操作将原子引用的值从 2022 改为 2024,并打印出修改的结果、当前版本号和当前值。
 * 由于线程 t2 使用了带有版本号的 CAS 操作,因此它能够正确地识别出 ABA 问题的存在,输出结果为 false 2022。
 * @author Administrator
 */
public class MyABAResolver {
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(2022, 1);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(2022, 2023, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(2023, 2022, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
        }, "t1").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(2022, 2024, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + result + "\t当前版本号:" + atomicStampedReference.getStamp() + "\t当前实际值:" + atomicStampedReference.getReference());
        }, "t2").start();

        TimeUnit.SECONDS.sleep(5);

        System.out.println("最终结果:" + atomicStampedReference.getReference());
    }
}

AtomicStampedReference

AtomicStampedReference 是 Java 中的一个原子类型,它是基于版本号的原子引用。在使用 AtomicStampedReference 解决 ABA 问题时,版本号可以用来识别对象是否发生了 ABA 问题。

AtomicStampedReference 内部维护了两个字段,分别是 private volatile V reference; 和 private volatile int stamp;,用来存储引用对象和版本号。

下面是 AtomicStampedReference 类的部分源代码解析:

public class AtomicStampedReference<V> {

    private volatile V reference;  // 存储引用对象
    private volatile int stamp;   // 存储版本号

    public AtomicStampedReference(V initialRef, int initialStamp) {
        reference = initialRef;
        stamp = initialStamp;
    }

    // 获取引用对象
    public V getReference() {
        return reference;
    }

    // 获取版本号
    public int getStamp() {
        return stamp;
    }

    // 带版本号的 CAS 操作
    public boolean compareAndSet(V expectedReference, V newReference,
                                  int expectedStamp, int newStamp) {
        synchronized (this) {
            if (expectedReference == reference && expectedStamp == stamp) {
                reference = newReference;
                stamp = newStamp;
                return true;
            } else {
                return false;
            }
        }
    }

    // 其他方法...
}

构造方法 AtomicStampedReference(V initialRef, int initialStamp) 用来初始化引用对象和版本号。getReference() 方法用来获取引用对象,getStamp() 方法用来获取版本号。

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) 方法是 AtomicStampedReference 类最重要的方法之一。在执行 compareAndSet 操作时,如果 expectedReference 和 expectedStamp 分别与 reference 和 stamp 相等,就会将 reference 和 stamp 更新为 newReference 和 newStamp。它用来进行带版本号的 CAS 操作,并且在操作成功时返回 true,操作失败时返回 false。这个方法是线程安全的,使用了同步锁 synchronized 来保证线程安全。

总的来说,AtomicStampedReference 类的实现基于版本号的原子性引用类型,通过版本号的更新和 CAS 操作的比较,保证了 ABA 问题的解决。同时,由于版本号是内置的,使用起来比自定义版本号解决器更加方便。

你可能感兴趣的:(多线程,记录,Java,java)