所谓乐观锁就是每次不加锁,假设没有冲突而去完成某项操作;如果发生冲突了那就去重试,直到成功为止。
CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false。
那什么是CAS机制呢?
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
这样说或许有些抽象,我们来看一个例子:
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值,所以B从11变成了12。此时对线程1来说,因为之前的内存值被别的线程修改过一次,所以A从10变成了A=11,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
CAS的优缺点:
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。但它也有缺点,如下:
1.CPU可能开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用悲观锁了。
3.ABA问题。
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
ABA问题:
线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。
ABA问题处理:
思路:解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操作时都对比此版本号。
要是更加通俗易懂的话 ,子月这里编了一个小故事。我们这里把乐观锁比作张三,张三有一个好兄弟,张三不久前买了一张床,当张三把床铺整理干净后就出去了,也没有锁门,因为张三是乐观锁,所以张三就是乐观的人,他出门不锁门,他相信他的兄弟是不会偷偷地睡他的床的。但是张三回家睡觉时,是会检查一下床铺的状态的。要是床铺状态是他当时弄成的状态,他就美美地睡一觉。可是因为张三没有锁门,他的兄弟偷偷地溜进了张三的房间,睡了一觉。临走的时候,还不忘把张三的床铺整理干净。但是张三再回家时,却发现这个床铺的状态不是他当时整理的了,所以张三就不会睡觉,进行自旋。
那么这里就相当于在计算机内部,会使用CPU资源,造成CPU的开销较大。
而ABA问题也很好理解 就好比张三的兄弟趁着张三外出,睡了两次,恰好,第二次整理的床铺的状态和张三临走的时候整理的状态一模一样。那么张三是不知道自己的床铺是被自己的兄弟睡过的。 这也就是CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的。
那么解决ABA问题就是加上一个版本的控制号,每次对于一个内存值的操作,都携带上一个版本号,CAS操作都会比对版本号。
那么张三的床上就相当于有了一个计数器,不论是谁,只要有人睡过,那么就会加1,到时候张三只要对应那个计数器就好了。
而悲观锁其实和上个故事差不多的。这里张三是悲观锁,他这个人很悲观的。他每次出门都会上锁,那么他的兄弟是不能进入房间的 那么便直接杜绝了所谓的内存值被别的线程修改。



