Java多线程:什么是ABA问题?
在 Java 多线程编程中,并发控制是核心难点之一。随着 JDK5 引入并发包(java.util.concurrent),原子类(如 AtomicInteger)和 CAS(Compare-And-Swap)操作成为解决轻量级同步问题的重要工具。然而,看似高效的 CAS 机制背后隐藏着一个容易被忽视的陷阱 ——ABA 问题。本文将从底层原理出发,结合具体案例和解决方案,全面解析这一并发编程中的经典问题
CAS
CAS 是一种无锁算法,这里不过多解释,具体请看另一篇:带你了解Java无锁并发CAS
比如这里有一个例子,想将原变量内容A通过CAS操作改为C:
public class ABA {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
// 尝试改为 C
System.out.println("change A->C {"+ref.compareAndSet(prev, "C")+"}");
}
}
这里只有一个主线程在对共享变量ref做操作,没有并发问题,因此是完全可以成功的:
main start...
change A->C {true}
其他线程
若是此时出现另一个线程抢先在主线程更改完毕中间就改变了共享变量的引用,比如在A–>C之前,线程1就已经将A–>B:
public class ABA {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
// 其他线程执行
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("change A->C {"+ref.compareAndSet(prev, "C")+"}");
}
private static void other() throws InterruptedException {
new Thread(() -> { // 将A-->B
System.out.println("change A->B {"+ref.compareAndSet(ref.get(), "B")+"}");
}, "t1").start();
}
}
此时由于CAS会先比较再修改,察觉共享变量的值已经被并发线程修改过了,因此CAS会返回false:
main start...
change A->B {true}
change A->C {false}
ABA问题
但是,假如有两个线程或多个线程在主线程CAS之前也对共享变量的值进行了修改,但是修改的最终结果又回到了A,于是在主线程CAS的时候就会认为该共享变量未被修改过,这就是ABA问题
public class ABA {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("change A->C {"+ref.compareAndSet(prev, "C")+"}");
}
private static void other() throws InterruptedException {
new Thread(() -> {// 将A-->B
System.out.println("change A->B {"+ref.compareAndSet(ref.get(), "B")+"}");
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {// 将B-->A
System.out.println("change B->A {"+ref.compareAndSet(ref.get(), "A")+"}");
}, "t2").start();
}
}
这里线程t1将A修改为B,而线程t2将B又修改回A,因此共享变量实际上已经发生了修改,但是由于值仍然是A,CAS就认为没有其他线程进行修改,执行结果就为true:
main start...
change A->B {true}
change B->A {true}
change A->C {true}
ABA问题的危害
ABA问题看似无害,因为最终的值仍然是A,CAS操作也成功执行了。然而,在实际应用中,ABA问题可能会引发严重的逻辑错误,尤其是在涉及到复杂数据结构或状态机的情况下。以下是一些可能的问题:
- 状态不一致:假设你使用CAS操作来管理一个状态机,而某个状态被错误地恢复到之前的状态,可能会导致程序逻辑错误。
- 资源泄漏:在某些情况下,ABA问题可能会导致资源泄漏。例如,如果你使用CAS操作来管理一个对象池,ABA问题可能会导致同一个对象被多次释放或错误地重用。
- 死锁或活锁:在多线程环境中,ABA问题可能会导致死锁或活锁,尤其是在复杂的同步机制中。
解决方案
为了解决ABA问题,Java提供了AtomicStampedReference
和AtomicMarkableReference
两个类。它们通过在CAS操作中引入版本号或标记来避免ABA问题。
AtomicStampedReference
AtomicStampedReference
通过在CAS操作中引入一个整数版本号来解决ABA问题。每次更新操作都会增加版本号,从而确保即使值相同,版本号也不同。
public class ABA {
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
System.out.println("main start...");
// 获取值 A 和版本号
int[] stampHolder = new int[1];
String prev = ref.get(stampHolder);
int prevStamp = stampHolder[0];
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("change A->C {"+ref.compareAndSet(prev, "C", prevStamp, prevStamp + 1)+"}");
}
private static void other() throws InterruptedException {
new Thread(() -> {// 将A-->B
int[] stampHolder = new int[1];
String prev = ref.get(stampHolder);
System.out.println("change A->B {"+ref.compareAndSet(prev, "B", stampHolder[0], stampHolder[0] + 1)+"}");
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {// 将B-->A
int[] stampHolder = new int[1];
String prev = ref.get(stampHolder);
System.out.println("change B->A {"+ref.compareAndSet(prev, "A", stampHolder[0], stampHolder[0] + 1)+"}");
}, "t2").start();
}
}
输出结果:
main start...
change A->B {true}
change B->A {true}
change A->C {false}
AtomicMarkableReference
AtomicMarkableReference
通过在CAS操作中引入一个布尔标记来解决ABA问题。每次更新操作都会改变标记的状态,从而确保即使值相同,标记也不同。
public class ABA {
static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);
public static void main(String[] args) throws InterruptedException {
System.out.println("main start...");
// 获取值 A 和标记
boolean[] markHolder = new boolean[1];
String prev = ref.get(markHolder);
boolean prevMark = markHolder[0];
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("change A->C {"+ref.compareAndSet(prev, "C", prevMark, !prevMark)+"}");
}
private static void other() throws InterruptedException {
new Thread(() -> {// 将A-->B
boolean[] markHolder = new boolean[1];
String prev = ref.get(markHolder);
System.out.println("change A->B {"+ref.compareAndSet(prev, "B", markHolder[0], !markHolder[0])+"}");
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {// 将B-->A
boolean[] markHolder = new boolean[1];
String prev = ref.get(markHolder);
System.out.println("change B->A {"+ref.compareAndSet(prev, "A", markHolder[0], !markHolder[0])+"}");
}, "t2").start();
}
}
输出结果:
main start...
change A->B {true}
change B->A {true}
change A->C {false}
总结
ABA问题是多线程编程中一个容易被忽视但潜在危害巨大的问题。通过使用AtomicStampedReference
或AtomicMarkableReference
,我们可以有效地避免ABA问题,确保程序在多线程环境下的正确性和稳定性。在实际开发中,尤其是在涉及复杂数据结构和状态管理时,务必注意这一问题,并采取相应的防护措施。