接触并发编程少不了CAS,这里不讲CAS,在另一篇文章里面有写CAS,这里只关注CAS的ABA问题。
什么叫CAS的ABA问题?
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
比如线程A操作值a,将值修改为b,这时线程B也拿到了a的值,将a改为b,再改为a,这时线程A比对值的时候,发送值还是和expect的一样,就会继续操作。如下面代码,T2的CAS操作是可以成功的。
private static AtomicInteger atomicInt = new AtomicInteger(100); public static void main(String[] args) throws InterruptedException { Thread intT1 = new Thread(new Runnable() { @Override public void run() { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); } }); Thread intT2 = 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); // true } }); intT1.start(); intT2.start(); intT1.join(); intT2.join(); }
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。
优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。
常见实践:“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。
java中的AtomicStampedReference就是使用的版本号来进行改进的。核心测试代码如下,这时候T2的CAS操作为false。同时打印了每个CAS操作时的stamp,cas1:0,cas2:1,cas3:0,这样在最终比对的时候,就不是简单的值比对了,还会加入版本号的比对。
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0); Thread refT1 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } System.out.println("cas1---"+atomicStampedRef.getStamp()); atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1); System.out.println("cas2---"+atomicStampedRef.getStamp()); 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("cas3---"+stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { } boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1); System.out.println(c3); // false } }); refT1.start(); refT2.start();
拜读一下源码,主要看下数据结构和CAS方法:
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, //增加了stamp的参数 int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }