目录
一、什么是CAS机制
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS中使用了三个操作数:内存地址V,旧的预期值A,要修改的新值B
更新一个变量时,只有当 变量的内存地址V的值和旧的预期值A相同时,才会把变量更新成B。
可能有点抽象,下面举个简单例子:
1.在内存空间中存储着值为10的变量:
2.此时线程1想要把变量增加1,即对线程1来说,旧的预期值A=10,要修改的新值B=11
3.在线程1要提交更新之前,另一个线程2抢先把内存地址V中的变量修改成11。
4. 这时线程1开始提交更新,首先进行A和内存V值的比较(Compare),A=10,内存V=11,明显不同,所以此时线程1提交更新失败。
5. 线程1重新获取内存V的值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7. 线程1进行SWAP,把地址V的值替换为B,也就是12。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
二、CAS的缺点
1.CPU开销大:在并发量很高的情况下,如果许多线程反复尝试更新一个变量,却一直更新不成功,循环往复会带给CPU很大压力。
2.不能保证代码块的原子性:CAS只能保证单个变量的原子性操作,但不能保证多个变量的原子性操作。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized
3.ABA问题
2.3 ABA问题
ABA问题,就是一个变量从A改成了B,又从B改成了A。
例如:
假设内存中有一个值为A的变量,存储在地址V中:
此时有三个线程想通过CAS更新这个变量的值,每个线程的执行时间有略微的偏差。线程1和线程2已经获得当前值,线程3还未获得当前值。
这时候线程1先工作,成功地把当前值从A改成B;同时线程2由于某些原因被阻塞住,没做更新操作,线程3在线程1更新之后,获得了当前值B。
再之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值”A,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
这个过程中,线程2获取到的变量值A是一个旧值,尽管和当前的实际值相同,但内存地址V中的变量已经经历了A->B->A的改变。
这看上去好像也没什么问题,但是结合具体场景而言,就看出问题了。
如:
1.举一个提款机的例子。假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
2.由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
3.理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。
4.线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。
5.线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。
6.线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50。
那么问题就来了,相当于扣多了一次50块钱。
2.4 ABA问题的解决方法
给变量加版本号。
在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
我们仍然以最初的例子来说明一下,假设地址V中存储着变量值A,当前版本号是01。线程1获得了当前值A和版本号01,想要更新为B,但是被阻塞了。
这时候,内存地址V中的变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
随后线程1恢复运行,进行Compare操作。经过比较,线程1所获得的值和地址V的实际值都是A,但是版本号不相等,所以这一次更新失败。
这其实就是乐观锁的做法,跟Mysql的乐观锁思想一样。
三、CAS的应用场景
但是Synchronized和CAS没有说哪一个更好,在并发量很高的情况下,反而用Synchronized会好一点。因为CAS属于乐观锁,如果频繁有线程对共享资源进行写操作的话,那么乐观锁就会失败,会重复判断共享资源是否被改变过,会耗费很大cpu资源。
出处: