问题
多线程情况下,每个线程使用CAS操作欲将数据A修改成B,当然我们只希望只有一个线程能够正确的修改数据,并且只修改一次。当并发的时候,其中一个线程已经将A成功的改成了B,但是在线程并发调度过程中尚未被调度,在这个期间,另外一个线程(不在并发中的请求线程)将B又修改成了A,那么原来并发中的线程又可以通过CAS操作将A改成B。
![](https://img.haomeiwen.com/i28430736/7441f69f87ebe952.png)
避免ABA问题
给变量加一个版本号,在比较的时候不仅要比较当前变量的值 ,还需要比较当前变量的版本号。在java1.5中,
已经提供了AtomicStampedReference来解决问题,检查当前引用是否等于预期值引用,
其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值
遇到ABA问题如何解决?
要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1。
1、第一种方式,使用AtomicStampedReference对象
AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号,当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。 对上面程序进行修改:
package com.lpl;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10, 1);
public static void main(String[] args) {
//张三线程去修改参考对象的值
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 拿到的当前时间戳版本号为:" + atomicStampedReference.getStamp());
//休眠1秒,为了让李四线程也拿到同样的初始版本号
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
//通过CAS自旋算法锁修改index的值
atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 10 -> 11 -> 10");
}, "张三").start();
//李四线程去读取内存值并设置新值
new Thread(() -> {
try{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 拿到的当前时间戳版本号为:" + stamp);
//线程休眠2秒,为了让张三线程完成ABA操作
TimeUnit.SECONDS.sleep(2);
//判断是否修改成功
boolean isSuccess = atomicStampedReference.compareAndSet(10, 12, stamp, atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 最新版本号:" + atomicStampedReference.getStamp() + ",是否修改成功:" + isSuccess + ",当前值是:" + atomicStampedReference.getReference());
}catch (InterruptedException e) {
e.printStackTrace();
}
}, "李四").start();
}
}
程序运行结果:
![](https://img.haomeiwen.com/i28430736/e141ef6643449594.png)
线程张三完成CAS操作,最新版本号已经变成3,与线程李四之前拿到的版本号1不相等,所以操作失败。 有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference对象,可以标识引用变量是否被更改过。
2、第二种方式,使用AtomicMarkableReference对象
修改代码为:
package com.lpl;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class ABA {
private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(10, false);
public static void main(String[] args) {
//张三线程去修改参考对象的值
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 当前参考对象是否被修改:" + atomicMarkableReference.isMarked());
//休眠1秒,为了让李四线程也拿到是否被修改的标识
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
//通过CAS自旋算法锁修改index的值
atomicMarkableReference.compareAndSet(10, 11, atomicMarkableReference.isMarked(), true);
atomicMarkableReference.compareAndSet(11, 10, atomicMarkableReference.isMarked(), true);
System.out.println(Thread.currentThread().getName() + " 10 -> 11 -> 10");
}, "张三").start();
//李四线程去读取内存值并设置新值
new Thread(() -> {
try{
boolean isMarked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName() + " 当前参考对象是否被修改:" + isMarked);
//线程休眠2秒,为了让张三线程完成ABA操作
TimeUnit.SECONDS.sleep(2);
//判断是否修改成功
boolean isSuccess = atomicMarkableReference.compareAndSet(10, 12, isMarked, true);
System.out.println(Thread.currentThread().getName() + " 当前修改状态为:" + atomicMarkableReference.isMarked() + ",是否修改成功:" + isSuccess + ",当前值是:" + atomicMarkableReference.getReference());
}catch (InterruptedException e) {
e.printStackTrace();
}
}, "李四").start();
}
}
程序运行结果为:
![](https://img.haomeiwen.com/i28430736/820ffdb6b7a2bdc4.png)
由于张三线程执行了ABA操作,李四线程一开始拿到的预期修改状态与操作时内存中的修改状态不一致导致操作失败,避免了ABA问题。
![](https://img.haomeiwen.com/i28430736/c0380037b4a74d85.png)
以上就是Android开发中的并发内存模型中-CAS导致ABA问题的解决方法。Android的学习进阶是一步一步积累的,想在Android继续深造进阶自己,可以参考这本电子笔记《Android核心进阶手册》,可点击获取免费方式.
文末
ABA的问题在于,pop函数9中, next = curr->next 和while之间,线程被切换走,然后其他线程先把A弹出,又把B弹出,然后又把A压入,栈变成了A--> C,此时head还是指向A,等pop被切换回来继续执行,就把head指向B了。
因此ABA问题的本质是内存回收的问题,对于上面的例子,就是A被弹出后,需要保证它的内存不能立即释放(因为还有线程引用它),也就不能立即被重用。这是新手使用CAS最常见的坑,实际项目中,通常配合128位CASQ、引用计数、序列号或者HazardPointer等技术来避免ABA问题。
网友评论