什么是CAS
在Java中,CAS是指"比较并交换"(Compare and Swap)操作.由于他是一个原子类操作在cpu上只有一条指令.他是一种多线程同步机制,用于实现非阻塞算法和并发控制.CAS包含三个变量:内存地址 value,寄存器上旧的值oldvalue,以及新的值B.
以AtomicInteger为例CAS的操作流程是:
- 将当前内存地址中的值value与寄存器中的值oldvalue进行比较
- 如果相等将新的值oldvalue+1的值赋给oldvalue并返回.
- 如果不等,进入循环将新的的value赋给oldvalue,再一次进行判断.
这样就可以判断在当前变量自增的时候是否有其他线程穿插进来了
在Java中,Java.util.concurrent包提供了一些原子类用于支持CAS操作.这些原子类提供了CAS的封装,可以直接使用他们来实现线程安全操作.
CAS引发的ABA问题
虽然CAS操作是一种无锁的并发控制装置,但并不适合所有的场景.在高并发的场景下多个线程频繁的征用一个CAS操作可能会导致ABA问题.进而引发不一致性.
举个栗子:
滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50操作.我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
正常的过程
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期 望更新为 50.
- 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
异常的过程
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期 望更新为 50.
- 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!)轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼!!
**解决方案 **
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).