场景回顾
去年我写过一篇处理系统中一个并发引起的bug提到了在高并发下如何避免数据状态不一致的问题,里面的解决方案其实已经用到了CAS
的思想了。具体的场景请查看文章。
传统的锁
多线程并发修改一份共享数据的时候,通常都是使用加锁的办法,来保证共享数据的正确性和安全性。但是使用锁是有代价的。当一个线程占有了一份共享资源的锁后,其他的线程会被阻塞住,只能等待,啥事情没法干,万一拥有锁的那个线程正在等待一个I/O操作的处理结果,那么其他线程可能需要等待更长的时间。JVM实现阻塞的方式就是挂起线程,之后重新调度被挂起的线程,这样就造成了上下文切换,CPU的吞吐量就大大的降低了。线程应该是要干活的,而不是一直在等待工作或者等待别的线程释放锁。
无锁
就像之前在处理系统中一个并发引起的bug一文提到的,不使用任何锁,同样可以在高并发的情况下,保证数据的正确性。这里其实就是CAS思想的体现了。
CAS
CAS的想法很简单,它包含3个参数CAS(V,E,N)
V:将要被更新的变量的值
E : 期望的值
N : 新值
只有当V=E的时候,才将N的值赋给V,否则说明其他线程已经修改过V的值了,已经捷足先登了,当前线程无需再做任何操作,直接退出返回当前V的值。
由于没有使用到锁,同时修改数据失败的线程也没有被挂起,以致变成等待状态。
CAS硬件指令实现
CAS是属于复合操作,如何保证这种【读(read)-改(update)-写(write)】操作的原子性呢?现在的CPU都有实现CAS指令,通过硬件的方式来保证CAS操作的原子性。
备注:
CPU底层实现其实也是要用到锁的,只不过这种复合操作的原子性由硬件来维持,应用层的代码无需关心而已。
AtomicInteger
AtomicInteger类中就有一个CAS操作:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset,expect,update);
}
compareAndSet方法利用JNI调用了操作系统的原生接口来达到修改数据的目的。
CAS的ABA问题
如果数据经历过如下变化:A–>B–A,这个时候,原来的线程看到数据还是A,跟预期的一样,就会直接做修改操作。在数据状态敏感的业务场景中,这样可能导致大故障的。
可以利用乐观锁的想法,加一个版本号来做区分。
CAS优缺点
优点
- 无锁,就不存在死锁和线程竞争资源时被挂起的问题,提高了CPU的利用率,避免了大量的上下文切换开销
缺点
1、上面提到的ABA问题
2、只能保证操作一个共享变量的原子性,如果操作多个共享变量,则无法保证原子性(此时只能用锁的机制了)
参考文章