悲观锁和乐观锁

悲观锁和乐观锁详解

何为悲观锁、乐观锁

​ 乐观锁对应于生活中对于事情积极乐观,总想把事情往好的方面发展,悲观锁则是把所有事情都预想成最差的结果,这两种人都各有优点,不能抛开场景直接定义优劣好坏。

悲观锁

​ 总是假设最坏的情况,每次去拿数据的时候都假设会被别人修改,所以每次在获取数据的时候都会去上锁,这样别人想拿到这条数据时就需要先获得这把锁(共享资源每次只给一个线程使用,其它线程阻塞,持锁线程使用完才会释放该锁,之后由其它线程去争抢),正如我们常用的关系型数据库mysql里面就用到了很多这种机制:行锁,表锁,读锁,写锁等等。都是在操作之前先上锁,java中的Synchronized和ReentrantLock等独占锁就是悲观锁的实现。

乐观锁

​ 总是假设最好的情况,每次去获取数据时,都假设别人不会改动,所以也就不去上锁,但是在更新的时候会去判断一下在此期间别人有没有去更新过,这个可以使用版本号的机制或者CAS实现(CAS compare and set 比较再set,提高性能的一种加锁方式,相信大家多少会有点了解,正如我们open组的CAS单点登入项目一样,先判断是否登陆过,有则可以在其他系统登入,无则先登入某个系统,后跳转,扯远了 ),乐观锁适用于多读的应用类型,这样可以提升吞吐量,像数据库提供的write_condition 机制, 其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 —— CAS 实现的。

两种锁的使用场景

​ 从上面对两种锁机制的介绍,我们从大致了解到他们各有优缺点,无法单纯的去定义好坏,像乐观锁这种适用于写比较少的情况下(读比写多),也就是冲突较少的情况,这样可以省去的锁的开销(简单介绍一下这个所谓的开销,熟悉sync的同学应该会有一定了解,其实就是os的用户态和内核态之间的切换,具体比较繁琐,我就不做赘述了),加大了系统的吞吐量。

​ 但是如果写多于读,那就会经常出现冲突,导致上层应用一直retry重试,反而降低了性能,这个时候就应该使用悲观锁机制去实现。

乐观锁的两种实现方式

版本号机制

​ 常用的这种方式,一般都是在表结构中增加一个version字段,表示数据更新完的版本(次数也行),当数据被修改时,version值加一,当线程A要更新数据值时,需要同时读取到version的值,并且在提交时,再次获取该值,如果大于数据库的version值,则入库,反之记录该值,然后再去获取一次,其实就是上面说的 CAS compare and set 比较再set 自我实现

CAS 算法

compare and swap 比较再交换,是一种比较有名的无锁算法。

无锁编程,即是在不使用锁的情况下实现多线程共享变量同步,也就是在没有线程阻塞的情况下实现变量同步,也叫非阻塞同步 就像NIO一样(借用一下NIO性质来表达),cas算法涉及到三个变量:需要读写的内存值值 V(也就是后面用来和A作比较),需要用来比较的值 A 需要写入的新值 B。只有当V = A 才会将B的值入库,否则会一直重新获取并且比较,就是一个自旋状态

乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

​ 如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA” 问题。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长,开销大

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

只能保证一个共享变量的原子操作

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

cas和Synchronize的使用场景

​ 简单来说cas适用于读比写多的情况,synchronize适用于写多于读的场景(读的冲突比写少,写的话冲突较多)

Synchronize

​ 就针对于共享资源竞争较少的情况而言,使用sync的同步锁进行线程阻塞和唤醒切换,以及用户态和内核态切换操作所造成CPU开销较大,我这里是不建议使用sync的,最好还是用cas来实现,因为cas基于硬件实现,是在jvm中操作的,不需要进入os内核,不需要切换线程,操作自旋几率较少,因此可以获得更好的性能。

CAS

​ 对于资源竞争严重(线程之间的争抢)的情况,cas自旋的概率会比较大,从而会比较占用cpu的资源,效率会低于sync。

最后再说一句在java并发编程中,sync一直都是一个元老级别的关键字,很久以前的版本,它一直是重量级锁,直至jdk1.6版本之后(为了减少获得锁和释放锁带来的性能消耗)才加入了 偏向锁,轻量级锁以及各种优化,使其变得不再那么 ”重“ ,sync的底层实现主要是依靠Lock-Free的队列,基本思路是自旋后阻塞(这里的自旋是10次,为了不让其过度自旋而消耗CPU资源而设置的,这里面其实涉及到了一个锁升级的过程),竞争切换后继续竞争锁,稍微牺牲了公平性(这里指的就是偏向锁),去获得高吞吐,在线程冲突较少的情况下,可以获得和CAS类似的性能,而线程冲突严重的时候,是远高于CAS的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值