1、ABA问题

问:谈一下原子类AtomicInteger的ABA问题?是否知道什么是原子更新引用?如何解决ABA问题
答:
1、CAS会导致“ABA问题”。

CAS算法实现的一个重要前提,是取出内存中某时刻的数据,然后比较并交换。在多线程情况下,就存在取出数据后,该数据被其他线程修改的情况。

1)比如线程1从主内存中取出的数据是A,然后进行一些业务操作(比如需要5s),最后并准备将该数据改为C;
2)此时线程2抢占到cpu资源,然后进行一些业务操作(比如需要2s),然后将该共享数据改为B;
3)此时线程1还在进行业务操作,因此线程3抢占到cpu资源,然后进行一些业务操作(比如需要2s),然后将该共享数据改为A;
此时,线程1的业务操作结束了,当它来进行cas时,发现该数据是A,比较并交换后,改为了C。

上面发生的步骤,就可以理解为CAS的ABA问题。

说明一下,如果只要求该数据最终结果是A,不关心中间是否有其他改动,那么该问题影响不大,或者说没有影响。

但是,举个例子,该数据是用户的钱,或者说是仓库的货物量。每一次操作应该都是受监督的,不可能说有人挪用了钱,或者挪用货物去卖了。当有人来查的时候,又将数据加了回去。虽然最终的数据是对的上的,但是肯定这样肯定是存在问题的。

我们要有技术能解决上面的问题。

2、除了ABA问题,在多线程情况下,之前解决n++的原子性问题,使用的是AtomicInteger类。那么如果要解决的是某一个自定义类的原子性问题呢?此时就可以使用原子更新引用:AtomicReference

3、要解决ABA问题,可以使用带时间戳的原子引用:AtomicStampedReference

要很好地理解本文提到的ABA问题,首先要充分理解CAS的概念和作用,不清楚的话,可以先看下这篇博文(Java基础:CAS详解)。

1、通过原子引用代码验证ABA问题

在前两篇博文中,说明原子性的时候,都是以n++进行举例的。

但是实际开发中,肯定还有很多自定义的类,此时也需要进行原子引用。那么就可以使用并发包下的原子引用:AtomicReference。

首先创建一个仓库货物类:WareHouseGoods。

package com.koping.test;

public class WareHouseGoods {
    public String name;

    public String type;

    public int number;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public WareHouseGoods(String name, String type, int number) {
        this.name = name;
        this.type = type;
        this.number = number;
    }

    @Override
    public String toString() {
        return "WareHouseGoods{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                ", number=" + number +
                '}';
    }
}

然后通过代码来验证下ABA问题,验证代码如下,运行结果如下图。

可以看到,在线程1处理逻辑时(那5s内),线程2自己销售了,然后又拿了一些货物回到仓库。
此时线程1发现仓库还是100件时,就直接取出并销售了。

丝毫不知道中间发生了ABA问题,有人拿了你的货物中间高卖低买可能已经赚过钱了,也可能以次充好给你放到仓库了。这些线程1居然都不知道,居然成功了。

package com.koping.test;

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

public class AbaDemo {
    static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
    static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
    static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);

    static AtomicReference<WareHouseGoods> atomicReference = new AtomicReference<>(wareHouseGoods1);

    public static void main(String[] args) {
        // 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3));
            System.out.println("现在仓库货物的总数为:" + atomicReference.get());
        },"线程1").start();

        new Thread(() -> {
            // 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
            // 然后再自己补回20件到仓库中,因此剩余还是100件。
            atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2);
            System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.get());
            atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1);
            System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.get());
        },"线程2").start();
    }
}

在这里插入图片描述

2、通过带时间戳的原子引用解决ABA问题

要解决上一小节中的ABA问题,可以时间带时间戳的原子引用:AtomicStampedReference。

1)每一批货物都是有版本号的,货物有初始版本号:1。
2)当线程1从数据库获取的版本号是1,它就还是去做业务操作了;
3)此时线程2私自将货物进行了取出和放回,虽然最终货物还是100件,单数版本号已经变为了3;
运行结果如下图,可以看到此时线程1获取50件货物的时候就失败了,因为发现版本号不是之前的版本号1了。这时候系统就知道有人动过仓库的货物了,因此可以进行追查。

package com.koping.test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AbaDemo {
    static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
    static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
    static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);

    // 初始版本号是1
    static AtomicStampedReference<WareHouseGoods> atomicReference = new AtomicStampedReference<>(wareHouseGoods1, 1);

    public static void main(String[] args) {
        // 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
        new Thread(() -> {
            // 先从数据库中获取之前的版本号,再进行业务操作
            int stamp = atomicReference.getStamp();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("\n线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3, stamp, stamp+1));
            System.out.println("现在仓库货物的总数为:" + atomicReference.getReference());
        },"线程1").start();

        new Thread(() -> {
            // 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
            // 然后再自己补回20件到仓库中,因此剩余还是100件。
            atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2, atomicReference.getStamp(), atomicReference.getStamp()+1);
            System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.getReference());
            System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
            atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1, atomicReference.getStamp(), atomicReference.getStamp()+1);
            System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.getReference());
            System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
        },"线程2").start();
    }
}

在这里插入图片描述