计算机中的乐观锁与悲观锁

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

所属分类 概念

相关标签 高并发自旋锁

关于锁

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

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

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

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

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

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,业务上就需要返回余额不足。

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

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

CAS 算法涉及到三个操作:

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

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

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

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

乐观锁问题

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 资源,效率低于悲观锁。

米虫

做一个有理想的米虫,伪全栈程序猿,乐观主义者,坚信一切都是最好的安排!

本站由个人原创、收集或整理,如涉及侵权请联系删除

本站内容支持转发,希望贵方携带转载信息和原文链接

本站具有时效性,不提供有效、可用和准确等相关保证

本站不提供免费技术支持,暂不推荐您使用案例商业化

选择个人头像

昵称

邮箱

QQ

网址

评论提示

  • 头像:系统为您提供了12个头像自由选择,初次打开随机为你选择一个
  • 邮箱:可选提交邮箱,该信息不会外泄,或将上线管理员回复邮件通知
  • 网址:可选提交网址,评论区该地址将以外链的形式展示在您的昵称上
  • 记忆:浏览器将记忆您已选择或填写过得信息,下次评论无需重复输入
  • 审核:提供一个和谐友善的评论环境,本站所有评论需要经过人工审核