https://juejin.im/post/5c87afa06fb9a049f1550b04
什么是CAS
CAS
即Compare And Swap
的缩写,翻译成中文就是比较并交换,其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,也就是CAS是原子性的操作(读和写两者同时具有原子性),其实现方式是通过借助C/C++
调用CPU指令完成的,所以效率很高。
CAS
的原理很简单,这里使用一段Java
代码来描述
public boolean compareAndSwap(int value, int expect, int update) {
// 如果内存中的值value和期望值expect一样 则将值更新为新值update
if (value == expect) {
value = update;
return true;
} else {
return false;
}
}
复制代码
大致过程是将内存中的值、我们的期望值、新值交给CPU进行运算,如果内存中的值和我们的期望值相同则将值更新为新值,否则不做任何操作。这个过程是在CPU中完成的,这里不好描述CPU的工作过程,就拿Java代码来描述了。
CAS的缺点
CAS
虽然高效的实现了原子性操作,但是也存在一些缺点,主要表现在以下三个方面。
ABA问题 【在CAS原子操作之前,线程被切换了】
在多线程场景下CAS
会出现ABA
问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下
- 线程1,期望值为A,欲更新的值为B
- 线程2,期望值为A,欲更新的值为B
线程1
抢先获得CPU时间片,而线程2
因为其他原因阻塞了,线程1
取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3
,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,
此时线程2
从阻塞中恢复,并且获得了CPU时间片,这时候线程2
取值与期望的值A比较,发现相等则将值更新为B,虽然线程2
也完成了操作,但是线程2
并不知道值已经经过了A->B->A
的变化过程。
ABA
问题带来的危害:
小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。
解决方法:
在变量前面加上版本号,每次变量更新的时候变量的版本号都+1
,即A->B->A
就变成了1A->2B->3A
。
循环时间长开销大
如果CAS
操作失败,就需要循环进行CAS
操作(循环同时将期望值更新为最新的),如果长时间都不成功的话,那么会造成CPU极大的开销。
这种循环也称为自旋
解决方法: 限制自旋次数,防止进入死循环。
只能保证一个共享变量的原子操作
CAS
的原子操作只能针对一个共享变量。
解决方法:
如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,或者可以把多个共享变量合并成一个共享变量进行CAS
操作。
CAS的应用
我们知道CAS
操作并不会锁住共享变量,也就是一种非阻塞的同步机制,CAS
就是乐观锁的实现。
- 乐观锁 乐观锁总是假设最好的情况,每次去操作数据都认为不会被别的线程修改数据,所以在每次操作数据的时候都不会给数据加锁,即在线程对数据进行操作的时候,别的线程不会阻塞仍然可以对数据进行操作,只有在需要更新数据的时候才会去判断数据是否被别的线程修改过,如果数据被修改过则会拒绝操作并且返回错误信息给用户。
- 悲观锁 悲观锁总是假设最坏的情况,每次去操作数据时候都认为会被的线程修改数据,所以在每次操作数据的时候都会给数据加锁,让别的线程无法操作这个数据,别的线程会一直阻塞直到获取到这个数据的锁。这样的话就会影响效率,比如当有个线程发生一个很耗时的操作的时候,别的线程只是想获取这个数据的值而已都要等待很久。
Java
利用CAS
的乐观锁、原子性的特性高效解决了多线程的安全性问题,例如JDK1.8中的集合类ConcurrentHashMap
、关键字volatile
、ReentrantLock
等。