CAS
什么是CAS
CAS:全称Compare and swap,字面意思:”比较并交换“,能够比较和交换某个寄存器中的值和内存中的值是否相等,如果相等,则把另一个寄存器中的值和内存进行交换。一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
CAS伪代码
下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解CAS 的工作流程.
boolean CAS(address, expectValue, swapValue) { if (&address == expectedValue) { &address = swapValue; return true; } return false; }
由于CAS是通过一条指令完成的,这也就给我们实现线程安全除了加锁以外又多了一条方式——无锁编程。但CAS的使用范围是有局限性的,加锁适用性更广
CAS是怎么实现的
主要是通过CPU硬件支持,软件层面才做得到
CAS有哪些应用
实现原子类
比如,多线程针对一个count
变量进行++,在Java标准库中已经提供了一组原子类
标准库中提供了 java.util.concurrent.atomic
包, 里面的类都是基于这种方式来实现的。其中的AtomicInteger
和AtomicLong
最常用,提供了自增/自减/自增任意值/自减任意值的操作。这些操作就是基于CAS按照无锁编程的方式来实现
private static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement();//count++ /* count.incrementAndGet();//++count count.getAndDecrement();//count-- count.decrementAndGet();//--count */ } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count.get());//获取count的值 } //100000
上述的原子类,没有进行任何的加锁,就是基于CAS来实现的
伪代码实现:
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; } }
①这里最开始value=0。此时赋值oldValue=value,oldValue为0。
②在循环里的CAS含义是value和oldValue比较是否相等,相等则将value=oldValue+1,返回的是false,不进入循环,最终返回oldValue即0,value是1,这就是相当于i++的效果。
③假设此时有别的线程在①的步骤后穿插进来,则会出现完整的自增流程,此时value=1,oldValue=0,因为还没有进行第二步
④此时重新回到②中,此时value!=oldValue,则函数返回false,将进入循环,将oldValue赋值为新的value=1,则oldValue为1,重新CAS函数比较,此时value和oldValue相等,则value=oldValue+1=2,返回true,退出循环,最终返回oldValue即1,value是2,这就是相当于i++的效果
当两个线程并发的执行i++操作的时候,如果不加任何限制,意味着这两个i++可能是串行的,则计算正确,也可能是穿插进行的,就会出现问题
实现线程安全:
- 加锁保证线程安全:通过锁,强制避免出现穿插
- 原子类/CAS保证线程安全:借助CAS来识别当前是否出现“穿插”的情况,如果没穿插,此时直接修改,就是安全的。如果出现穿插了,就重新获取内存中最新的值,再次尝试修改
实现自旋锁
public class SpinLock { private Thread owner = null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程(Thread.currentThread()). while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner = null; } }
此处owner
表示当前是哪个线程持有这把锁。null
解锁状态
Thread.currentThread()
获取当前线程引用。哪个线程调用lock()
,这里得到的结果就是哪个线程的引用
当该锁已经处于加锁状态,这里就会返回false
,CAS就不会进行实际的交换操作,接下来循环条件成立,继续进入下一轮循环。但循环体为空,就又进行这个函数判断,因此一瞬间就执行很多轮次,这样就达到了自旋锁效果。一旦有另一个线程把锁释放了,这里就可以立即得到那个锁
CAS的ABA问题
CAS的关键要点是比较寄存器1和内存的值,通过这里的是否相等,来判定内存的值,是否发生了改变。
如果内存值变了,存在其他线程进行了修改;
如果内存值没变,没有别的线程修改,接下来进行的修改就是安全的。
问题是这里的值没变,就一定没有别的线程修改吗?
A-B-A:另一个线程从A->B,又从B->A,此时本线程区分不了,这个值始终没变,还是出现了变化又回来了的情况。
CAS判定的是“值相同”,实际上我们期望的是"值没有变化过"
解决方案:(约定"值"只能增长不能减小,这个值不是要修改的值,是下面引入的“版本号”)
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
- CAS 操作在读取旧值的同时, 也要读取版本号
- 真正修改的时候,
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
- 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
CAS 也是属于多线程开发中的一种典型的思路
但是咱们在这里并没有介绍 Java 中提供的 CAS 的api 怎么用,也没介绍系统中提供的 CAS api 咋用
实际开发中,一般不会直接使用 CAS,都是用库里已经基于 CAS 装好的组件(像原子类这种)
后面如果大家被问到了原子类咋实现的,自旋锁咋实现的,就可以用CAS解答了