在将如何去解决去解决CAS的ABA问题时,我们先来说一下什么是CAS,CAS全称 compare-and-swap ,CPU并发原语。执行是连续的,不允许被中断,不会造成数据不一致问题。
CAS算法:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
然后我们再讲一讲ABA问题。
ABA问题: 假定在某个时刻某个线程从内存中取出A,然后在下个时刻准备更新这个值;在这个时间差内数据发生了改变;比如 假设线程1从内存中取出了A,线程2也从内存中取出了A,并且将值修改为B,最后又改为A,当线程1去更新值得时候发现内存中的数据和线程备份数据相同,可以更新;但是此时内存中的值其实发生了变化的,只不过又变回去了。
又比如现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:head.compareAndSet(A,B);
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,,而对象B此时处于游离状态:
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
其中栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
我们用一段简单的代码来演示一下ABA问题:
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread T1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread T2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3);
}
});
T1.start();
T2.start();
}
}
我们在线程T1中将atomicInt 的值改成101,又改回100,然后在线程T2中再次将atomicInt 的值改成101,由于线程T2是晚于线程T1执行的,所以T2拿到的atomicInt的值其实是已经变化了又变回来的值,然而它还是能够成功改变atomicInt的值。这就是ABA问题。
解决方式:由于CAS算法没有直接的使用锁;而是通过乐观锁的方式去控制并发的;而对于乐观锁而言一般都是操作+时间戳来控制每一次的版本号的;在JDK类库中,可以使用AutomicStampReference来解决。
然后我们来演示一下如何去使用AutomicStampReference来解决它。
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
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) {
}
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();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(c3);
}
});
refT1.start();
refT2.start();
}
}
由于在第一个线程中修改过atomicStampedRef的值,所以它的版本号也变更了,而在第二个线程中由于先拿到atomicStampedRef的版本号,然后二线程睡眠了一段时间,这段时间里atomicStampedRef的值已经变换过了,版本号就不同了,所以线程二的操作就失败了。