Java基础-0x02:探究CAS引出的ABA问题

Fuji

JUC

由CAS缺陷到ABA问题

谈谈什么是ABA问题:狸猫换太子

Thread0x00, Thread0x01。 0x01比0x00 用时少,同时操作主内存。由于0x01比0x00快,所以主内存中数据已经被改了。0x01再次读取内存中的值,再次修改主内存中的值。几次循环之后,0x00执行完毕,写回主内存时,主内存的值正好是期望值,然后0x00修改成功。

表面上是风平浪静的,但是在0x00执行过程中,0x01已经修改过多次。 这就叫ABA问题

ABA问题是怎么产生的

CAS会导致”ABA问题“。

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

具体发生过程在上面引言部分已经写过,不再赘述。

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

原子引用

java.util.concurrent.atomic.AtomicReference<V>

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
/**
* JUC的包中给我们提供了AtomicInteger、AtomicLong、AtomicBoolean等<br>
* 被原子包装的数据结构。<br>
*
* 如果我们希望是一个AtomicUser,AtomicOrder,AtomicCustomer这养的类也是没有问题的<br>
* JUC中为我们提供了AtomicReference<V>这个类
*
*
* @author allwayz
*/
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<User> userAtomicReference = new AtomicReference<>();
User z3 = new User("z3",22);
User l4 = new User("l4",23);

userAtomicReference.set(z3);
System.out.println(
userAtomicReference.compareAndSet(z3,l4)+
"\t current "+userAtomicReference);

System.out.println(
userAtomicReference.compareAndSet(z3,l4)+
"\t current "+userAtomicReference);
}
}

如何解决ABA问题?

  • 原子引用
  • 增加一种机制:修改版本号(类似时间戳)

时间戳原子引用

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* ABA问题的解决<br>
*
* AtomicStampedReference
*
* @author allwayz
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

public static void main(String[] args) {
new Thread(()->{
//模拟ABA
atomicReference.compareAndSet(100,101);
System.out.println("current number: " + atomicReference.get());
atomicReference.compareAndSet(101,100);
System.out.println("current number: " + atomicReference.get());
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
},"t1").start();

new Thread(()->{
try {
//暂停t2线程1秒,保证t1线程先执行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(atomicReference.compareAndSet(100,101)+
"\t Result: " + atomicReference.get());
},"t2").start();

try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(">>>>>>>>>>>>>>>>>>>以下是ABA问题的解决<<<<<<<<<<<<<<<<<<<<");

/**
* T3线程拿到一开始的版本号,然后暂停1秒中,等T4线程拿到一样的版本号,
* 然后暂停3秒钟,确保T3线程执行一次ABA操作。
*
*/
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

//执行ABA操作
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());
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
},"t3").start();

new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+
"\t 修改结果: "+result+
"\n 实际版本号:"+atomicStampedReference.getStamp()+
"\n 实际最新值:"+atomicStampedReference.getReference());
},"t4").start();

}
}

引入了原子时间戳之后,线程执行中共享变量被修改的问题就不会逃过检测,也就保证了线程安全。