乐观锁和悲观锁

参考:

什么是CAS和ABA问题?如何解决? - 知乎 面试官:你了解乐观锁和悲观锁吗?CAS 是如何实现的?

目录

一、悲观锁

二、乐观锁

三、怎么实现乐观锁 

(一)版本号机制

具体实现:

例子:

(二)CAS 算法 

例子:

 ABA问题:


一、悲观锁

  • 悲观锁顾名思义就是:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。Java 中的 synchronizedReentrantLock 是悲观锁的典型实现方式。
  • 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
  • 悲观锁通常用于并发环境下的数据库系统,是数据库本身实现锁机制的一种方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题

二、乐观锁

  • 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。
  • Java 中的 AtomicIntegerLongAdder 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。在数据库中,乐观锁并不会使用数据库提供的锁机制,一般在表中添加 version 字段或者使用业务状态来实现。
  • 乐观锁直到提交时才锁定,所以不会产生任何死锁。
  • 乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。 

三、怎么实现乐观锁 

(一)版本号机制

  • 版本号机制是通过比较版本号确保数据一致性
  • 具体实现:

  • 一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

  • 例子:

    假设数据库中帐户信息表counters中有一个 version 字段,设当前值为 1 、当前帐户余额字段(cash )为 $1000 。

  1. 操作员 A 此时将其读出 version=1 ,并从其帐户余额中扣除($1000-$500=$500 )。

  2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息 version=1 ,并从其帐户余额中扣除 ($1000-$200=$800 )。

  3. 操作员 A 先完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额$500 ,提交至数据库并更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 的值+1,更新为 2 。

  4. 操作员 B 也完成了操作,也将版本号( version=1 )试图向数据库提交数据余额$800 但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

(二)CAS 算法 

  • CAS 的全称是 Compare And Swap(比较与交换),被广泛应用于各大框架中。
  • CAS 的思想是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
  • CAS 是一个原子操作(即最小不可拆分的操作,也就是操作一旦开始,就不能被打断,直到操作完成),底层依赖于一条 CPU 的原子指令。

CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)

  • E:预期值(Expected)

  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

例子

线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6)。

  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。

  2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

 ABA问题:

上面的例子还会出现一种问题,就是ABA问题

      比如  :因为程序的问题,启动了两个线程1,2

  1. 线程1 取款$100:从数据库余额表中 获取余额(要更新的值V= 200 元),与预期值E= 200 元比对成功,取出 $100 ,更新余额为100,所以拟写入的新值N=100 
  2.  线程2 取款$100:从数据库余额表中要更新的值V= 200 元,进程阻塞等待修改。
  3. 线程3 接收转账¥100:从数据库余额表中要更新的值V= 100 元 ,与预期值E= 100 比对成功,接收转账 ¥100 ,更新余额为200 ,所以拟写入的新值N=200
  4.  线程2:取款$100:恢复执行,要更新的值V= 200 元,与预期值E= 200 元对比成功,取出 $100 ,更新余额为100,所以拟写入的新值N=100 
  5. 最终的结果是 余额为100 元。但实际应该是200元,(200-100+100=200)

ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了,这就是 ABA 问题。

  • 解决 ABA 问题的一种方法是使用带版本号的 CAS,也称为双重 CAS(Double CAS)或者版本号 CAS。

  • 因此,每次进行 CAS 操作时,不仅需要比较要更新的变量值与期望的值是否相等还需要比对数据库版本号是否相等。如果相等,才进行修改操作。所以不仅要读取要更新的变量值,还需要读取数据版本号 version这样即使变量的值从 A 变成了 B 再变成了 A,版本号也会发生变化,从而避免了误判。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值