乐观锁与悲观锁简介


文章参考于:

乐观锁
  • 顾名思义,乐观锁对应于生活中乐观的人,总是想着事情往好的方向发展。
  • 总是假设最好的情况,每次在使用数据的时候都认为别人不会修改,所以在使用数据的时候不会上锁,但是在更新数据的时候会判断(可以使用版本号CAS算法)在此期间被人有没有更新了这个数据。
  • 乐观锁适用于读多写少的场景,冲突真的很少发生,省去了锁的开销,提高了系统的整个吞吐量 。
悲观锁
  • 悲观锁对应生活中悲观的人,总是认为事情会向坏的情况发展。
  • 总是假设最坏的情况,每次使用数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想使用这个数据就会阻塞,直到他拿到锁。(共享资源每次只给一个线程使用,其他线程阻塞,用完之后再把资源转让给其他线程)
  • 传统的关系型数据库用到了很多这种锁机制
  • 悲观锁适用于读少写多的场景,会减少冲突的产生,从而提高系统的性能。

这里两种锁各有优缺点,不能片面的认为某一个比另一个好,既然某一个出现了,肯定有他出现的理由。

乐观锁的两种实现方式(版本号机制、CAS算法)
版本号机制
  • 一般在数据表中添加一个数据版本号(version)字段,标识数据被修改的次数,当数据被修改的时候,version值加一
  • 当线程A更新数据时,在读数据的同时也会读取version值,在提交更新时,若读数据时读取的version值与提交时读取到的version值相等是才更新数据,并且version值加一,否则重试更新操作,直到更新成功。
  • 举个例子:
    • 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
      1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
      2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
      3. 操作员 A 完成了修改工作,将数据版本号加一(version=2),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
      4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
    • 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
CAS算法
  • compare and swap(比较与交换),是一种有名的无锁算法,无锁编程,即 不使用锁的情况下实现多线程之间的变量同步,也就是没有线程被阻塞的情况下实现变量的同步,

    所以也叫非阻塞同步(Non-blocking Synchronization)

  • CAS算法涉及到三个操作数

    1. 需要读写的内存值 V
    2. 进行比较的值 A
    3. 准备写入的新值 B
  • 当且仅当 V == A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作),一般情况下是一个自旋操作,即 不断的重试

乐观锁的缺点
  • ABA问题
    • 如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到仍是A值,这就能说明他的值未被其他线程修改过吗?很显然不行,因为在这段期间他的值可能被改为其他值,然后又改回了A,那CAS操作就会误认为他从未被修改过。
    • JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 循环时间长开销大
    • 自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  • 只能保证一个共享变量的原子操作
    • CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
CAS和synchronized的使用情景
  • 简单来说CAS适用于 多读场景,冲突一般较少;synchronized 适用于 多写场景,冲突一般较多。
  • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充:

  • Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞竞争切换后继续竞争锁稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值