一、问题

(1)什么是ABA?

(2)ABA的危害?

(3)ABA的解决方法?

(4)AtomicStampedReference是什么?

(5)AtomicStampedReference是怎么解决ABA的?

二、简介

AtomicStampedReference是java并发包下提供的一个原子类,它能解决其它原子类无法解决的ABA问题。

三、ABA

ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。

比如,两个线程按下面的顺序执行:

(1)线程1读取内存位置X的值为A;

(2)线程1阻塞了;

(3)线程2读取内存位置X的值为A;

(4)线程2修改内存位置X的值为B;

(5)线程2修改又内存位置X的值为A;

(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;


可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。

ABA问题通常发生在无锁结构中,用代码来表示上面的过程大概就是这样:

public class ABATest { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(1); new Thread(()->{ int value = atomicInteger.get(); System.out.println("thread 1 read value: " + value); // 阻塞1s LockSupport.parkNanos(1000000000L); if (atomicInteger.compareAndSet(value, 3)) { System.out.println("thread 1 update from " + value + " to 3"); } else { System.out.println("thread 1 update fail!"); } }).start(); new Thread(()->{ int value = atomicInteger.get(); System.out.println("thread 2 read value: " + value); if (atomicInteger.compareAndSet(value, 2)) { System.out.println("thread 2 update from " + value + " to 2"); // do sth value = atomicInteger.get(); System.out.println("thread 2 read value: " + value); if (atomicInteger.compareAndSet(value, 1)) { System.out.println("thread 2 update from " + value + " to 1"); } } }).start(); }}

打印结果为:

thread 1 read value: 1thread 2 read value: 1thread 2 update from 1 to 2thread 2 read value: 2thread 2 update from 2 to 1thread 1 update from 1 to 3

四、ABA的危害

为了更好地理解ABA的危害,我们还是来看一个现实点的例子。

假设我们有一个无锁的栈结构,如下:

public class ABATest { static class Stack { // 将top放在原子类中 private AtomicReference top = new AtomicReference<>(); // 栈中节点信息 static class Node { int value; Node next; public Node(int value) { this.value = value; } } // 出栈操作 public Node pop() { for (;;) { // 获取栈顶节点 Node t = top.get(); if (t == null) { return null; } // 栈顶下一个节点 Node next = t.next; // CAS更新top指向其next节点 if (top.compareAndSet(t, next)) { // 把栈顶元素弹出,应该把next清空防止外面直接操作栈 t.next = null; return t; } } } // 入栈操作 public void push(Node node) { for (;;) { // 获取栈顶节点 Node next = top.get(); // 设置栈顶节点为新节点的next节点 node.next = next; // CAS更新top指向新节点 if (top.compareAndSet(next, node)) { return; } } } }}

咋一看,这段程序似乎没有什么问题,然而试想以下情形。

假如,我们初始化栈结构为 top->1->2->3,然后有两个线程分别做如下操作:

(1)线程1执行pop()出栈操作,但是执行到if (top.compareAndSet(t, next)) {这行之前暂停了,所以此时节点1并未出栈;

(2)线程2执行pop()出栈操作弹出节点1,此时栈变为 top->2->3;

(3)线程2执行pop()出栈操作弹出节点2,此时栈变为 top->3;

(4)线程2执行push()入栈操作添加节点1,此时栈变为 top->1->3;

(5)线程1恢复执行,比较节点1的引用并没有改变,执行CAS成功,此时栈变为 top->2;

What?点解变成 top->2 了?不是应该变成 top->3 吗?

那是因为线程1在第一步保存的next是节点2,所以它执行CAS成功后top节点就指向了节点2了。

测试代码如下:

private static void testStack() { // 初始化栈为 top->1->2->3 Stack stack = new Stack(); stack.push(new Stack.Node(3)); stack.push(new Stack.Node(2)); stack.push(new Stack.Node(1)); new Thread(()->{ // 线程1出栈一个元素 stack.pop(); }).start(); new Thread(()->{ // 线程2出栈两个元素 Stack.Node A = stack.pop(); Stack.Node B = stack.pop(); // 线程2又把A入栈了 stack.push(A); }).start();}public static void main(String[] args) { testStack();}

在Stack的pop()方法的if (top.compareAndSet(t, next)) {处打个断点,线程1运行到这里时阻塞它的执行,让线程2执行完,再执行线程1这句,这句执行完可以看到栈的top对象中只有2这个节点了。

记得打断点的时候一定要打Thread断点,在IDEA中是右击选择Suspend为Thread。

通过这个例子,笔者认为你肯定很清楚ABA的危害了。

五、ABA的解决方法

ABA的危害我们清楚了,那么怎么解决ABA呢?

笔者总结了一下,大概有以下几种方式:

(1)版本号

比如,上面的栈结构增加一个版本号用于控制,每次CAS的同时检查版本号有没有变过。

还有一些数据结构喜欢使用高位存储一个邮戳来保证CAS的安全。

(2)不重复使用节点的引用

比如,上面的栈结构在线程2执行push()入栈操作的时候新建一个节点传入,而不是复用节点1的引用;

(3)直接操作元素而不是节点

比如,上面的栈结构push()方法不应该传入一个节点(Node),而是传入元素值(int的value)。

好了,扯了这么多,让我们来看看java中的AtomicStampedReference是怎么解决ABA的吧

六、源码分析

(一)内部类

private static class Pair { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static  Pair of(T reference, int stamp) { return new Pair(reference, stamp); }}

将元素值和版本号绑定在一起,存储在Pair的reference和stamp(邮票、戳的意思)中。

(二)属性

private volatile Pair pair;private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();private static final long pairOffset = objectFieldOffset(UNSAFE, "pair