一、ABA问题的产生
CAS 在修改变量值时,会先检查该变量的值是否和预期值一致,若一致则修改,引发的ABA问题的情况是:如一个变量初始值为A,被另外一个线程修改成B,再由B修改为A,此时使用CAS进行操作就检查不出变量的变化轨迹,并对该变量修改,这就是ABA问题,其前提条件是“节点可以被循环使用”。
下面通过例子来模拟ABA问题:
public class Node {
public final String item;
public Node next;
public Node(String item){
this.item = item;
}
}
/**
* 模拟ABA问题
*/
public class ConcurrentStack {
AtomicReference<Node> topNode = new AtomicReference<>();
/**
* push
* @param node
*/
public void push(Node node){
Node oldNode;
do {
oldNode = topNode.get();
node.next = oldNode;
} while(!topNode.compareAndSet(oldNode, node));
}
public Node pop(int time){
Node newTop;
Node oldTop;
do {
oldTop = topNode.get();
if(oldTop == null){
return null;
}
newTop = oldTop.next;
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!topNode.compareAndSet(oldTop, newTop));
return oldTop;
}
// 测试
public static void main(String[] args) throws InterruptedException {
ConcurrentStack stack = new ConcurrentStack();
stack.push(new Node("A"));
stack.push(new Node("B"));
// 节点B出栈
Thread thread1 = new Thread(() -> {
// 设置线程1执行出栈比较久,用了5s
Node node = stack.pop(5);
System.out.println(Thread.currentThread()+" pop B: "+node.item);
},"thread1");
thread1.start();
// 线程2执行出栈之后再入栈:先让A和B出栈,
// 然后D,C,B入栈(B在栈顶)
Thread thread2 = new Thread(() -> {
// 先让A和B出栈
Node B = stack.pop(0);
stack.pop(0);
// 入栈
// 注意:线程2实现了节点的循环利用,
// 它先将栈里面的内容全部出栈,然后入栈,最后栈顶的内容是之前出栈的Node
stack.push(new Node("D"));
stack.push(new Node("C"));
stack.push(B);
System.out.println(Thread.currentThread()+" pop B: "+B.item);
},"thread2");
thread2.start();
thread1.join();
thread2.join();
System.out.println("main pop C : "+stack.pop(0).item); // 预期结果弹出C
System.out.println("main pop D : "+stack.pop(0).item); // 预期结果弹出D
}
}
在测试时:
1) 先入栈A、B元素;
2) 设置线程1执行出栈,但出栈前会休眠一段时间,目的是先让线程2先执行出栈入栈任务;
3) 线程2执行出栈操作之后再进行入栈操作:先让A和B出栈,然后让D,C,B入栈(B为原来的节点对象,此时B在栈顶);
4) 线程2执行完后,切换到线程1执行任务,弹栈;
5) 预想:最后main线程弹出C、D节点。
实际运行结果如下:
Thread[thread2,5,main] pop B: B
Thread[thread1,5,main] pop B: B
main pop C : A
Exception in thread "main" java.lang.NullPointerException
at com.java.juc.ABA.ConcurrentStack.main(ConcurrentStack.java:75)
从结果看出,预期弹出C节点却弹出了A,因为线程1中的B的node.next为A,而节点C,D丢失,这就是ABA问题造成的严重后果。
二、解决方法
前一篇文章也提到了,可以使用锁的方式来解决,当然JDK 1.5 之后也提供了带邮戳引用的原子类(AtomicStampedReference和AtomicMarkableReference)来解决该问题,当且当前的引用等于期望的引用,stamp和期望的stamp相等才设置 reference 和 stamp 为给定的更新值 。
下面是AtomicStampedReference的例子:
public class ConcurrentStack {
AtomicStampedReference<Node> top = new AtomicStampedReference<Node>(null,0);
public void push(Node node){
Node oldTop;
int v;
do{
v=top.getStamp();
oldTop = top.getReference();
node.next = oldTop;
}
while(!top.compareAndSet(oldTop, node,v,v+1));
// }while(!top.compareAndSet(oldTop, node,top.getStamp(),top.getStamp()+1));
}
public Node pop(int time){
Node newTop;
Node oldTop;
int v;
do{
v=top.getStamp();
oldTop = top.getReference();
if(oldTop == null){
return null;
}
newTop = oldTop.next;
try {
TimeUnit.SECONDS.sleep(time);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
while(!top.compareAndSet(oldTop, newTop,v,v+1));
// }while(!top.compareAndSet(oldTop, newTop,top.getStamp(),top.getStamp()));
return oldTop;
}
public void get(){
Node node = top.getReference();
while(node!=null){
System.out.println(node.getItem());
node = node.getNode();
}
}
}
从代码中可以看到,每次修改都会将newStamp+1,从而避免ABA问题。
参考:
1.《JAVA并发编程实战》,Doug lea
2. 浅谈Java中ABA问题及避免