📖聊一聊计算机中的乐观锁与悲观锁

发布: 2020-05-15
热度: 44
趋势: 44
权重: 0
🎯

乐观锁与悲观锁本质上没有好坏区分,各有优缺点,所应对的业务场景有所区别,锁的核心还是为了解决并发场景下可能带来的数据不一致的问题

关于锁

悲观锁

何谓悲观,就是假象所有的事情都是向坏的方面发展。

当线程去取用数据的时候都认为别人有可能会去修改它,因此取数据时会进行上锁操作。

数据上锁后,如果其他人向拿这部分数据,就需要阻塞等待,直到锁被解除。

比如:共享数据每次只给一个线程使用,其它线程阻塞,用完后再转让给其它线程。

常见悲观锁

传统的关系型数据库里就用到这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。(当然 JDK1.8 后 synchronized 做了一系列的优化,加入了自旋锁和偏向锁)

乐观锁

所谓乐观,就是假象所有的事情都是向好的方面发展。

当线程去取用数据的时候都认为别人不会动这部分数据,因此不会主动进行上锁行为。

而在更新数据之前,会判定一下数据是不是产生过变化。

乐观锁其实并不是锁,也不会产生锁的行为,但是会循环执行与重试,从而保证数据的一致性。

常见乐观锁

据库提供的 write_condition 机制,其实都是提供的乐观锁。

常用的实现形式是版本号机制和 CAS 算法实现。

Java 中 java.util.concurrent.atomic 包下面的原子变量类,使用的 CAS 实现乐观锁。

使用场景

悲观锁与乐观锁本质上没有好坏区分,各有优缺点,所应对的业务场景有所区别。

乐观锁适用于写比较少的情况下(多读场景),即很少发生冲突,省去了锁的开销,加大系统吞吐量。

悲观锁适用于多写的情况,经常产生冲突,如果使用乐观锁会导致应用会不断的进行重试,降低性能。

乐观锁实现

悲观锁就是硬性对数据锁定,因此没有什么特别的实现。

乐观锁一般会使用版本号机制或 CAS 算法实现。

版本号

数据中增加一个版本号字段,每次修改数据的时候会自增这个版本号。

线程要更新数据操作时,先读取数据也会读取版本号,正式提交更新,需要用读取的版本号和数据库中最新数据的版本号相当方执行操作。

如果发现版本号产生变化,则需要重新读取数据再次执行刚刚的处理操作,直到更新成功。

举例:

  1. 数据库中存在用户余额表,余额 100,当前版本号是 100。
  2. A 线程,读取数据,余额-50=50,记录版本号为 100,准备更新。
  3. B 线程和 A 线程同时读出了数据,余额-100=0,记录版本号为 100,准备更新。
  4. A 线程成功更新了数据,数据库当前余额 50,版本号 101。
  5. B 线程开始提交更新,发现版本号不是 100,重新读出最新数据,再次执行余额-100=-50,业务上就需要返回余额不足。

CAS 算法

Compare And Swap(比较与交换)是一种有名的无锁算法。

不使用锁的情况下实现多线程之间的变量同步,在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blockingSynchronization)。

CAS 算法涉及到三个操作:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

仅当 V == A 时,CAS 通过原子方式用新值 B 来更新 V 的值。

否则不会执行任何操作(比较和替换是一个原子操作)。

一般情况下是一个自旋操作,即不断的重试。

乐观锁问题

ABA 问题

ABA 问题是实现最常见的问题。

因为版本号或者变量是可变的,如果某个线程将变量有 A 改为 B,然后又改成了 A。

在同一时间的其他线程会误以为没有产生更新,就会导致覆写的问题。

ABA 问题的处理方案就是在确认修改前,我们要知道变量有没有被修改过或者修改了几次,从而判断是否需要执行重试。

JDK1.5 以后的 AtomicStampedReference 类就提供了此种能力,compareAndSet 方法就是首先检查当前引用是否等于预期引用。

其主要用于确认变量是否被修改过,当前标志是否等于预期标志,全部相等,则以原子方式将修改值。

长时间循环

CAS 的本质就是不断重试(不成功就一直循环执行直到成功)。

长时间不成功,会给 CPU 带来非常大的执行开销。

如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用。

  1. 可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现,一些处理器上延迟时间是零。
  2. 可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。

变量局限性

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

比如 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性。

可以把多个变量放在一个对象里来进行 CAS 操作。

小结

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用悲观锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 CPU 资源。 乐观锁往往基于硬件或应用实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,乐观锁自旋重试的概率会比较大,从而浪费更多的 CPU 资源,效率低于悲观锁。
当前文章暂无讨论,留下脚印吧!
大纲
  • 关于锁
    • 悲观锁
      • 常见悲观锁
    • 乐观锁
      • 常见乐观锁
  • 使用场景
  • 乐观锁实现
    • 版本号
    • CAS 算法
  • 乐观锁问题
    • ABA 问题
    • 长时间循环
    • 变量局限性
  • 小结
提交成功,请等待审核通过后全面展示!

发表评论

昵称
邮箱
链接
签名
评论

温馨提示:系统将通过浏览器临时记忆您曾经填写的个人信息且支持修改,评论提交后仅自己可见,内容需要经过审核后方可全面展示。

选择头像