前言
在前面《并发编程之CAS算法与原子变量详解》我们采用JUC包下的Atomic原子变量,解决了多线程环境下共享变量原子性问题,Atomic底层操作是基于CAS算法,并且也提到,采用一种无锁的非阻塞算法的实现,乐观锁算法,但是也会有一些缺点。
其中有一个就是ABA问题,CAS原理其实就是拿副本中的预期值与主存中的值作比较,如果相等就继续替换新值,如果不相等就说明主存中的值已经被别的线程修改,就继续重试,这期间如果并发请求过多,有其中一步慢,就有可能出现问题,本文就来重点分析CASABA问题并怎么解决。
一、什么是ABA问题
ABA问题是指,当一个线程T1从内存地址X中取出值A,另一个线程T2也从内存地址X中取出值A,然后T2进行了一系列操作将值改变成B,写回主物理内存。接着,T2又将内存地址为X的数据变为A,这时线程T1进行了CAS操作发现内存中仍然是A,于是T1操作成功。尽管线程T1的CAS操作成功,但并不代表这个过程就没有问题,实际地址已经改变了。
ABA问题的产生原因是,CAS操作只检查内存中的数据值是否与预期值相同,而不会检查该值在内存中的变化过程。因此,当数据值在内存中发生变化时,CAS操作可能会误判为该值未被其他线程修改过。
编辑
简单描述:
假如说你有一个值,我拿到这个值是0,想把它变成2,我拿到1用cas操作,期望值是1,准备变成2,对象是Object,在这个过程中没有一个线程改过我这个值,肯定可修改。如果有一个线程在这个过程中把这个1修改成了2后来又变回1,中间值更改过但是不影响我后面的操作,这就是ABA问题。
二、怎么解决ABA问题
如果是int类型的,最终值是你期望的,没有关系。确实想要解决的话,就是加版本,做任何一个值的修改,修改完加一,后面检查的时候连同版本号一起检查。但是对于共享对象,如果有线程改变了对象的值,但是对象的地址其实没有变,这样其他线程在读取拿到的对象跟期望会认为是一样的,依然修改成功。所以针对对象,必须严格控制。
1、ABA问题
我们在讲解原子变量的时候,使用了AtomicInteger保证多线程共享变量原子性,但是一个线程A由于等了一会,这个间隙第一个线程B已经操作了这个数据,但是另一个线程A不清楚的,以为值还是之前的,导致修改成功。如果判断加一之后,才可以调用接口,线程A有可能在这个期间调用了业务接口,然后在吧数据改回去。
public class ABADemo {
// AtomicInteger也是会出现ABA
private static AtomicInteger atomicInteger = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0);
public static void main(String[] args) throws InterruptedException {
// 其中一个线程改了,又改回去
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.compareAndSet(100, 101);
// 这期间处理业务,比如说调用业务接口
if(atomicInteger.get().equals(101)){
System.out.println("todoAAA");
}
atomicInteger.compareAndSet(101, 100);
}
});
// 另一个线程由于等了一会,这个间隙intT1已经操作了这个数据,但是intT2不清楚的,以为值还是之前的,导致修改成功
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = atomicInteger.compareAndSet(100, 101);// true
System.out.println("thread intT2: " + atomicInteger);
System.out.println("flag is " + flag);
if(atomicInteger.get().equals(101)){
System.out.println("todoAAA");
}
}
});
intT1.start();
intT2.start();
}
}
运行结果如下,可以看到,第一个线程修改的修改了之后,调用业务比如todoAAA,然后在改回去,第二线程以为没有修改,同样可以修改数据,再次调用业务接口todoAAA,最终业务接口会被调用两次。
编辑
2、原子对象解决ABA问题
我们已经看到AtomicInteger虽然保证多线程共享变量原子性,但是会出现ABA问题,当然,要解决这个问题JUC也是提供了原子对象AtomicStampedReference来解决这个问题。
编辑
其实原理也是增加了版本号控制,每次操作之后,修改的值是否改变,版本号都+1,所以上面ABA问题可以改成如下代码:
public class ABADemo {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0);
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100,101,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101,100,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() + 1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());
boolean flag = atomicStampedRef.compareAndSet(100,101,stamp,stamp + 1);
System.out.println("thread refT2: " + atomicStampedRef.getReference() + ", flag is " + flag);
}
});
refT1.start();
refT2.start();
}
}
可以看到运行结果,线程2睡眠唤醒之后,发现版本号stamp变成2了 ,这个导致前后读取期望的版本号不一致了,线程2就不能在修改。这样的话,线程1及时偷偷调用了业务接口,线程2也会发现,并且不会在调用。
编辑
总结
总之,解决ABA问题是并发编程中的一个重要问题。解决ABA问题的归根到底还是使用版本号。在每个变量值修改时,增加一个版本号,并将版本号一起进行CAS操作。如果CAS操作失败,可以通过比较版本号来判断是否存在ABA问题。如果版本号相同,说明存在ABA问题;如果版本号不同,说明变量值已经被其他线程修改了
评论区