乐观锁与悲观锁详解

何谓悲观锁与乐观锁?

乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会对数据进行修改,所以每次拿数据时候都会上锁。因此在整个数据处理过程中,将数据处于锁定状态。这样别人想拿到这个数据就会block直到拿到锁。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

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

Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

悲观锁按照使用性质划分:

  • 共享锁(Share locks,简记为S锁)。

       共享锁也称读锁,事务A对对象T加了S锁,则其他事务也只能对对象T加S锁,多个事务可以同时读,但是不能有写操作,直到A事务释放S锁。

  • 排它锁(Exclusivelocks,简记为X锁)

        排它锁也称为写锁,事务A对对象T加X锁之后,其他事务不能再对对象T加任何锁,只有事务A可以读写对象直到事务A释放X锁。

  • 更新锁(简记为U锁)

      更新锁用来预定要对此对象施加X锁。它允许其他事务读,但是不允许再施加U锁或X锁。当被读取的对象将要被钢芯时,升级为X锁。主要是用来方式死锁的。因为使用共享锁时,修改数据的操作分两步,首先获得一个共享锁,读取数据,然后共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或者多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改之前直接申请更新锁,在数据修改的时候在升级为排它锁,就可以避免死锁。

悲观锁按照作用范围划分:

  • 行锁:

       行锁的作用范围是行级别,数据库能够确定哪些行需要锁的情况下用行锁,如果不知道会影响哪些行的时候就会使用表锁。

       举例如下:一个用户表User,有主键id和用户生日birthday,当使用update … where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update … where birthday=?这样的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。

  • 表锁:

       表锁的作用范围是整张表。


乐观锁:

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会去判断一下在此期间别人有没有去更新,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的两种实现方式:

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

1、版本号机制:

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

举例如下:

假设数据库中账户信息表中有一个version字段,当前值为1;而当前账户余额字段balance为¥100。

1、操作员A此时将其读出(version = 1),并从账户余额中扣除¥50(¥100-¥50)。

2、在操作员A操作的同时,操作员B也读入此用户信息(version = 1),并从其账户余额中扣除¥20(¥100-¥20)。

3、操作员A完成了修改工作,将数据版本号加1(version = 2),连同账户扣除后余额(balance = ¥50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。

4、操作员B完成了修改工作,将数据版本号加一(version = 2),试图向数据库提交数据(balance = ¥80),但是此时对比数据库记录版本时发现,操作员B提交的数据版本号为2,而数据库记录当前版本也为2,不满足“提交版本必须大于当前版本才能执行更新”的乐观锁策略,因此,操作员的提交也被驳回。这样就避免了操作员B用基于version = 1的旧数据修改的结果覆盖操作员A的操作结果的可能。

2、CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

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

1、需要读写的内存值V

2、进行比较的预期值A

3、拟写入的新值B

程序执行时,线程1从共享内存中取值V并建一个副本A,对A进行计算后将新的值保存为B,然后对A值和内存中的V值进行比较,如果A等于V,则认为内存中的V值没有被其他线程修改过,可以将新值B赋给内存,否则,认为内存中已被其他的线程修改,则重新执行计算操作和检测,直到旧的期望值A等于内存值V为止,这就是CAS自旋。( 比较和替换是一个原子操作)

当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。如图2-5-4-1,有两个线程可能会差不多同时对某内存操作,线程二先读取某内存值作为预期值,执行到某处时线程二决定将新值设置到内存块中,如果线程一在此期间修改了内存块,则通过CAS即可以检测出来,假如检测没问题则线程二将新值赋予内存块。

 

乐观锁的缺点

1、ABA问题

如果一个变量A初次读取的时候是A值,并在准备赋值的时候查到它仍然是A值就说明他的值没有被其他线程修改过了吗?那名显是不能的,因为在这段时间它的值可能被改为其他值,然后又改为A,那CAS操作就会误认为它从来没有被修改过。这个问题称为CAS操作的“ABA”问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2、循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升。pause指令有两个作用,第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的执行效率。

CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。但是从JDK5开始,提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。所以我们可以通过锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

两种锁的使用场景:

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。但是,乐观锁适用于多读的情况(写比较少的情况),这种情况下冲突很少发生,这样省去了锁的开销,加大了系统的整个吞吐量。但是如果经常发生冲突,上层就会不断的进行retry,这样反倒是降低了性能,所以写多的情况下用悲观锁就比较合适。

总结:乐观锁适用于多读场景;悲观锁适用于多写场景。

CAS(乐观锁)与synchronized(悲观锁)的使用场景

简单来说CAS适用于写比较少的情况(多读场景,冲突一般较少),synchronized适用于写比较多的情况(多写场景,冲突一般比较多)。

1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费CPU资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率小,因此可以获得更高的性能。

2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized,所以采用synchronized方法。

 

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值