系列文章目录
一:计算机模型&volatile关键字详解
二:java中的锁体系
三:synchronized关键字详解
五:Atomic原子类与Unsafe魔法类详解
六:CAS下ABA问题及解决
文章目录
一、CAS问题引入
在并发问题中,最先想到的无疑是互斥同步,但线程阻塞和唤醒带来了很大的性能问题,同步锁的核心无非是防止共享变量并发修改带来的问题,但不是任何时候都有这样的竞争关系。
二、CAS是什么
CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,则交换要更新的值,也称乐观锁,是一种常见的降低读写锁冲突,保证数据一致性的乐观锁机制。
如线程甲从主内存中拷贝了变量A为1,在自己的线程中将副本A改为了10,当线程甲准备把这个变量更新到主内存时,如果主内存A的值不改变(期望值),还是1,那么线程甲成功更新主内存中A的值。但如果主内存A的值已经先被其他线程改掉不为1,那么线程甲不断地重试,直到成功为止(自旋)
CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false
三、ABA问题分析
1、问题的引入
- 银行存取钱
- 小偷偷钱
2、ABA问题代码
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
CompletableFuture.runAsync(()->{
int a = atomicInteger.get();
System.out.println("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+"--Cas修改后操作数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
});
CompletableFuture.runAsync(()->{
atomicInteger.incrementAndGet();// 1+1 = 2;
System.out.println("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
atomicInteger.decrementAndGet();// atomic-1 = 2-1;
System.out.println("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
});
}
执行结果:线程2获取到值等于1,此时线程3得到的值也是1,此时+1等于2,然后再减1等于1,此时线程1 更新操作,期望值是1,更新值是2,所以能更新成功,但是在这期间已经存在线程3对此值进行了两次操作。
3、并发业务场景ABA导致的问题已经解决
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,在有些情况下,“值”相同不会引入错误的业务逻辑(例如订单库存),有些情况下,“值”虽然相同,却已经不是原来的数据了
3.1、问题
系统中,某个商品的总库存为5,用户1和用户2同时下单购买
用户1购买了3个库存,于是库存要设置为2
用户2购买了2个库存,于是库存要设置为3
这两个设置库存的接口并发执行,库存会先变成2,再变成3,导致数据不一致(实际卖出了5件商品,但库存只扣减了2,最后一次设置库存会覆盖和掩盖前一次并发操作)
3.2、原因
出现数据不一致的根本原因,是设置操作发生的时候,没有检查库存与查询出来的库存有没有变化,理论上:
仅库存为5的时候,用户1的库存设置2才能成功
仅库存为5的时候,用户2的库存设置3才能成功
实际执行的时候:
库存为5,用户1的set stock 2确实应该成功
库存变为2了,用户2的set stock 3应该失败掉
3.3、解决
在上面例子中,执行更新操作时,带上版本号即可
四、ABA问题解决
优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。
1、AtomicStampReference
AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
public V getRerference();
public int getStamp();
public void set(V newReference,int newStamp);
2、AtomicStampReference的使用实例
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
CompletableFuture.runAsync(()->{
int stamp = atomicStampedRef.getStamp(); //获取当前标识别
System.out.println("操作线程" + Thread.currentThread()+ "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
try {
Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作线程" + Thread.currentThread() + "stamp="+stamp + ",CAS操作结果: " + isCASSuccess);
countDownLatch.countDown();
});
CompletableFuture.runAsync(()->{
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
countDownLatch.countDown();
});
countDownLatch.await();
}
执行结果:线程2获取到值等于1,标记值也是0,此时线程3得到的值也是1,标记值也是0,此时+1等于2,标记值+1是1;然后再减1等于1,标记值+1等于2,此时线程1 更新操作,期望值是1,标记值是0,更新值是2,所以不能更新成功
总结
其实除了AtomicStampedReference类,还有一个原子类也可以解决,就是AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用