什么是ABA问题?

ABA问题是由CAS而导致的一个问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间差内会导致数据的变化。

比如说一个线程一从内存位置V中取出A,这是另一个线程二也从内存中取出A,并且线程二进行类一些操作将值变成了B,然后线程二又将V位置的数据变成A,这时候线程一进行CAS操作发现内存中仍然是A,然后线程一操作成功!

尽管线程一的CAS操作成功,但是不代表这个过程就是没有问题的。

image-20200926110749346

就比如你家每天,都有一个B流浪汉来你家进行生活,他每天在你回家的时候将家整理成你离开的样子,不留下痕迹。你每天回到家后没有发现什么异常于是就正常生活。虽然他目前没有对你的生活造成什么问题,但是这个过程是很有隐患的!!!

总的来说以前的CAS操作,只管过程,也就是开头和结尾是否相同,如果相同就进行操作!而不管你中间到底进行来什么操作!!

原子引用

如何解决ABA问题呢?首先我们看一下原子引用。我们先前已经使用过java.util.concurrent.atomic下的基本数据类型原子操作类。但是缺少对对象的操作的原子类。也就是AtomicReference

image-20200926111835157

image-20200926111904734

我们来写一个案例来感受一下。

image-20200926112224920

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class User{
String userName;
int age;

public User(String userName, int age) {
this.userName = userName;
this.age = age;
}

@Override
public String toString() {
return "Offer2019.User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}

public class AtomicReferenceDemo {

public static void main(String[] args) {

User zs = new User("zs", 22);
User ls = new User("ls", 25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zs);

System.out.println(atomicReference.compareAndSet(zs,ls)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zs,ls)+"\t"+atomicReference.get().toString());

}
}

image-20200926112245588

atomicReference.set(zs);讲zs设为当前值

第一个atomicReference.compareAndSet(zs,ls)是将当前值和预期值比较,两者相同都为zs,所以更新为ls,返回true。

第二个atomicReference.compareAndSet(zs,ls)将当前值与预期值比较,当前值为ls预期值为zs比较失败,更新失败,返回false。

带时间戳的原子引用

我们将用到另一个新的类AtomicStampedReference,每次修改值后带有一个版本号!

image-20200926112833269

image-20200926112904785

解决ABA问题

案例

解决ABA问题之前我们先来用代码演示一下什么是ABA问题

image-20200926113834666

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ABADemo {
//static方便直接使用
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

//ABA问题
public static void main(String[] args) {

new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();


new Thread(() -> {
try {
//暂停1秒,保证t1已经完成了一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2020)+"\t"+atomicReference.get());
}, "t2").start();
}
}

image-20200926113909631

这就是ABA问题。

解决

解决ABA问题我们就要使用带时间戳的原子引用AtomicStampedReference。首先查看API

image-20200926114808050

image-20200926115406562

AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);首先给定初始值为100,初始版本号为1。

t3线程一开始通过getStamp()方法拿到目前的初始版本号,后暂停一秒,确保t4线程启动也拿到目前初始的版本号,t4线程启动后暂定3秒,从而保证t3线程的继续执行。t3线程首先通过compareAndSet()方法将预期值,和预期版本与当前值,当前版本分别对比只有两者都相同才将两值更新成更新的值。t3首先将100更新成101,版本号+1变成2。接着将101又更新成了100,版本号+1变成了3,t3线程完成ABA操作。

此时t4线程被调度执行接着继续执行操作,通过compareAndSet()将预期值和当前值比较同为100,但是预期版本号为1,当前版本号已经变成了3两者不一致!!所以更新失败!通过每次操作后版本的变化,从而解决了ABA问题的发生。

image-20200926142348525

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ABADemo {
//static方便直接使用
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

//ABA问题
public static void main(String[] args) {
//ABA问题的解决
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();//版本号
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
try {
//确保t4线程拿到初始版本号
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());

}, "t3").start();

//ABA问题的解决
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
try {
//确保t3线程完成ABA操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 2020, stamp, atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp() + "\t当前值为:" + atomicStampedReference.getReference());
}, "t4").start();
}
}