Java并发中的ABA问题学习与解决方案
什么是ABA问题?
在 Java 并发编程中,多个线程同时访问同一个共享变量时,由于线程调度不确定性,可能导致读写出现交叉,进而出现意料之外的问题。其中比较典型的就是 ABA 问题。
ABA 问题的简介来说,就是:线程1将共享变量A的值由原来的值A1修改为A2,然后又将A2修改为A1;这时线程2也来操作变量A,判断变量A的值仍然为A1,认为之前的操作未产生影响,然后执行其他操作。但是这个操作过程中忽略了 A 变量的第二次修改,这时候可能会造成意料之外的结果。
如何理解ABA问题?
ABA 问题与你跟好友到网吧玩 CS 或 PUBG 过程中非常相似。假设你开头去了洗手间,好友在那里等你进场。你误认为他不在,随意找了人一起玩起了 CS/PUBG,然后第三把结束之后又去洗手间了,这时你的好友回到了你们座位旁,看到你不见了,然后你回来时他还在这里,于是你们继续愉快的玩游戏。这种情况就是 ABA 问题:A 变量的状态 1 -> 2 -> 1,而你(线程 2)在看到状态为 1 的时候不知道它还经历了状态 2。
ABA问题的解决方案
方法一:使用AtomicStampedReference
解决这个问题的主要方案是使用 AtomicStampedReference
类,它通过使用版本戳(Stamp)的机制来解决这个问题。修改过程中,除了记录变量的值外,还会记录一个版本号,所以就不仅仅修改一个值了。
public class ABAProblem {
private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(1, 1);
public static void solveABAProblem() {
new Thread(() -> {
int stamp = value.getStamp();
System.out.println(Thread.currentThread().getName() + "第一次获取的stamp:" + stamp);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = value.compareAndSet(1, 10, stamp, ++stamp);
System.out.println(Thread.currentThread().getName() + "执行CAS操作结果:" + result);
}, "Thread 1").start();
new Thread(() -> {
int stamp = value.getStamp();
System.out.println(Thread.currentThread().getName() + "第一次获取的stamp:" + stamp);
boolean flag = true;
while (flag) {
stamp = value.getStamp();
Integer integer = value.getReference();
if (integer == 1) {
if (value.compareAndSet(integer, 5, stamp, ++stamp)) {
System.out.println(Thread.currentThread().getName() + "执行CAS操作修改成功" + ",当前stamp:" + stamp + ",修改后的值:" + value.getReference());
flag = false;
}
}
}
}, "Thread 2").start();
}
public static void main(String[] args) {
solveABAProblem();
}
}
这段代码中,我们创建了两个线程,第一个线程 Thread 1
将整数变量 value
的初始值设置为1。然后在1500毫秒的间隔之后又将 value
的值从1改成10,以及我们设定的初始版本号1,然后,等待2秒钟,让第二个线程运行起来。线程 Thread 2
首先也获取 value
的初始版本号 1,然后不断进行这个操作:如果 value
的值为 1,则尝试将其修改成 5。
运行后的结果如下所示:
Thread 1第一次获取的stamp:1
Thread 2第一次获取的stamp:1
Thread 2执行CAS操作修改成功,当前stamp:2,修改后的值:5
Thread 1执行CAS操作结果:false
在这段代码中,我们已经解决了 ABA 问题。这是因为我们使用了 AtomicStampedReference
,对每次写入值都进行了版本号的更新,如果在另一个线程已经修改过变量时,版本号已经变更,即使写入了相同的值,也会导致 CompareAndSet 失败。这个例子中,CAS 操作成功的线程2,它的操作步骤是先获取当前 AtomicStampedReference 的状态,再进行 CompareAndSet 操作,由于写入值的同时,我们还修改了版本号,所以对应的版本号不同,操作失败。而实际上,此时变量 value
的状态早已经改变,并不是期望的值1了,从而避免了 ABA 问题的发生。
方法二:使用Java 8的StampedLock
Java 8 中新增了一种锁类型 StampedLock
,它也可以防止 ABA 问题的出现。
public class StampedLockDemo {
private static final StampedLock lock = new StampedLock();
private static int shareValue = 1;
public static void read() {
long stamped = lock.tryOptimisticRead();
int result = shareValue;
if (!lock.validate(stamped)) { // 读取到的值不一致,需要用悲观读锁重新读取
stamped = lock.readLock();
try {
result = shareValue;
} finally {
lock.unlockRead(stamped);
}
}
System.out.println(Thread.currentThread().getName() + ": value=" + shareValue);
}
public static void write() {
long stamped = lock.writeLock();
try {
shareValue = shareValue + 1;
} finally {
lock.unlockWrite(stamped);
}
}
public static void solveABAProblem() {
new Thread(() -> {
int expectedValue = shareValue;
int newValue = shareValue + 1;
System.out.println(Thread.currentThread().getName() + "尝试将值从" + expectedValue + "修改为" + newValue);
write();
System.out.println(Thread.currentThread().getName() + "将值从" + expectedValue + "改为" + newValue);
}, "Thread 1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": value=" + shareValue);
read();
int expectedValue = shareValue;
int newValue = shareValue + 1;
System.out.println(Thread.currentThread().getName() + "尝试将值从" + expectedValue + "修改为" + newValue);
write();
System.out.println(Thread.currentThread().getName() + "将值从" + expectedValue + "改为" + newValue);
}, "Thread 2").start();
}
public static void main(String[] args) {
solveABAProblem();
}
}
这段代码中,Thread 1
和 Thread 2
分别以 "写" 和 "读" 的方式访问了共享变量 shareValue
。为了模拟一个 ABA 问题,Thread 1
先把 shareValue
从初始值 1 改成 2,然后又改回 1。这样的操作其实就是 ABA 问题,而在 Thread 2
中的读操作里,我们使用了 tryOptimisticRead
方法,它可以获取当前的版本戳,并在写操作时不加锁。如果在 Thread 2
执执行过程中写入变量的线程已经修改过值,那么在 validate
方法中会判断版本戳是否一致,如果不一致,则用悲观读锁重新读取变量。
运行结果如下所示:
Thread 1尝试将值从1修改为2
Thread 2: value=1
Thread 2: value=1
Thread 2尝试将值从1修改为2
Thread 2将值从1改为2
Thread 1将值从2改为1
Thread 2尝试将值从2修改为3
Thread 2将值从2改为3
从结果中可以看出,我们已经解决了 ABA 问题。这是因为在读写过程中,通过使用 StampedLock
,我们构造出了一个版本戳,并保证了戳码的线性,这样一来,就可以避免发生 ABA 问题。
总结
ABA 问题在 Java 并发编程中常常发生,它能够让开发者感到很困惑。解决 ABA 问题的方式有很多,比如:使用 AtomicStampedReference、使用 Java 8 的 StampedLock、使用 synchronized 等。如果你想了解更多高质量的多线程编程知识,可以参考《Java 并发编程实战》这本书。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java并发中的ABA问题学习与解决方案 - Python技术站