Java并发编程——深入理解乐观锁和悲观锁

1.悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。 这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。

2.乐观锁

乐观锁认为自己在使用数据的时候不会有别的线程来修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被别的线程更新,则根据不同方式执行不同操作(例如报错或者自动重试)。

可以根据版本号机制和 CAS 算法实现。

乐观锁适合多读少写的应用类型或者场景,即冲突真的很少发生的场景,这样省去了锁的开销,加大了系统的吞吐量。但是如果多写少读的情况,一般会经常发生冲突,这样会导致上层应用层不断 retry,这样反而降低了性能,所以一般建议多写的场景下使用悲观锁比较合适。

lock

 

2.1乐观锁常见的实现方式

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

2.1.1 版本号机制

在数据表增加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时, version 值加1。当线程1更新数据的时候,先拿到数据并读取出 version 值,修改完数据进行提交更新的时候时,若读取出的 version 值为当前数据库中 version 值相等时才更新数据,并将版本号字段加一,否则重试更新操作,直到更新成功。(此时进行失败重试)

举个例子: 假设数据库中账户信息表有一个字段 version,值为1;当前账户余额为100。当需要对账户信息表进行更新的时候,需要读取 version 字段,以及账户余额信息

  • 用户 A 读出数据:version = 1,balance = 100。从账户余额中扣除 50, balacne = 50
  • 用户 B 比用户 A 刚刚晚一点点时间,读出数据 :version = 1, balance = 100。从账户余额中扣除 20,balance = 80
  • 用户 A 完成修改操作,需要提交更新,但是在更新之前会先判断数据库中的版本号 version 值和自己读取到的 version 值是否一致,如果一致,则将版本号 version 字段的值加1(version = 2),连同账户扣除后的余额(balance = 50),提交到数据库服务器执行更新操作,此时由于提交数据中版本号大于数据库记录中的版本,则数据被更新,数据库记录 version = 2
  • 用户 B 完成修改操作,同样在更新之前先读取数据库中的版本号 version 值和自己读取到的 version 值是否一致,但此时发现自己读取到的 version = 1,数据库中的 version = 2,很显然不满足“当前最后更新的版本号 version 与操作员第一次读取到的版本号 version 相等”的乐观锁策略,因此用户 B 的提交被驳回。

这样,就避免了用户 B 基于 version = 1 的旧数据修改的结果覆盖用户 A 操作的结果,

2.1.2 CAS 算法

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

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

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V,否则不会执行任何操作。比较和替换是一个原则操作。一般情况下是一个自旋操作,即不断的重试

2.1.3 乐观锁的缺点

  • ABA 问题 如果一个变量 V 初次读取的时候的值为 A,并且在准备赋值的时候检查到变量 V 的值仍然是 A,那么可以说是 V 的值从来没被其他线程修改吗?很明显不能,因为有可能变量 V 的值,从 A 变到 B,然后又改回到 A,那么 CAS 的标准就会认为变量 V 从来没被修改过,这类问题被成为 CAS 的 ABA 问题。

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

  • 循环时间长、开销大 自旋 CAS (也就是不成功就一直循环操作直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作 CAS 只对单个变量共享有效,当操作涉及到多个共享变量时,CSA 无效。

        解决方案:从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

3.CAS与synchronized的使用情景

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

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于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、付费专栏及课程。

余额充值