目录
前言
前面我们了解了线程安全问题的解决方法,现在我们来讲讲悲观锁和乐观锁
一、悲观锁
顾名思义悲观锁就是认为线程的安全问题非常容易出现,对代码进行上锁。上篇文章所讲的锁机制都属于悲观锁。悲观锁的锁定和释放需要消耗比较多的资源,降低程序的性能。
二、乐观锁
乐观锁默认情况下认为数据是不会发生冲突的,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误信息,让用户决定如何去做。
乐观锁的实现
1.版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
2.CAS 算法
即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步,所以也叫非阻塞同步。CAS 算法涉及到三个操作数:
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。一般情况下,这是一个自旋操作,即不断的重试。
三、悲观锁和乐观锁对比
1.悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
2.乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
四、CAS算法存在的问题
CAS机制最大的问题就是ABA问题。
假设内存中有一个值为A的变量,存储在地址V中。
此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
看起来这个例子没啥问题,但如果结合实际,就可以发现它的问题所在。
我们假设一个提款机的例子。假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
由于提款机硬件出了点问题,小灰的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下,应该一个线程更新成功,一个线程更新失败,小灰的存款值被扣一次。
线程1首先执行成功,把余额从100改成50.线程2因为某种原因阻塞。这时,小灰的妈妈刚好给小灰汇款50元。
线程2仍然是阻塞状态,线程3执行成功,把余额从50改成了100。
线程2恢复运行,由于阻塞之前获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以会成功把变量值100更新成50。
原本线程2应当提交失败,小灰的正确余额应该保持100元,结果由于ABA问题提交成功了。
怎么解决呢?加个版本号就可以了。这样在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
总结
以上就是今天要讲的内容,本文简单介绍了悲观锁和乐观锁。欲知后事如何,且听下回讲解。