博主
258
258
258
258
专辑

乐观锁

尘微 2023-02-19 12:32:58 3402 0 0 0

乐观锁的实现两种方式

乐观锁大部分是基于CAS来实现的,即Compare And Swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS操作包含三个操作数——内存位置的值(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
我们使用一个例子来解释相信你会更加的清楚。

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的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。

注:CAS算法的缺点
【1】循环时间长开销很大:自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
【2】只能保证一个共享变量的原子操作:只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。
【3】ABA 问题:因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。

1.版本号机制—基于CAS 无ABA问题

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败
    图片alt
    mybatis-plus有现成的乐观锁插件,具体使用方法详见官网

乐观锁插件 | MyBatis-Plus
MyBatis-Plus 官方文档
https://baomidou.com/pages/0d93c0/#optimisticlockerinnerinterceptor

2.状态机制-基于CAS算法-有ABA问题

利用sql语句天然的原子性去实现状态机方案的乐观锁

核心sql:update table set amount = amount-#{buys} where id = #{id} and amount - #{buys} > 0;

核心sql2: update table set status where id = #{id} and status = 上一状态

通过乐观锁和一定的业务逻辑出来来解决幂等问题,可以灵活去思考。当你明白幂等的含义

什么是幂等:

幂等在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或者幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变,例如“getUsername()”和“setTrue()”函数就是一个幂等函数,更复杂的操作幂等保证是利用唯一交易号(流水号)实现,我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果是一样的