乐观锁和悲观锁剖析

怎么定义乐观锁和悲观锁?

乐观和悲观是一种态度,展示了站在不同的角度看待问题的方式。乐观的人认为事情总会往好的方向发展,悲观的人认为事情总会往坏的方向发展,角度不同,采取的措施也不同。不能说谁好谁坏,各有优缺点。

悲观锁

共享的资源每次只能给一个线程使用,其他线程处于阻塞状态,用完之后才释放资源给其他线程使用。传统的关系型数据库里就用了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在操作之前先上锁。Java中的synchronizedReentrantLock等独占锁就是悲观锁的思想实现。

想了解sychronziedlock的区别,详细见文章https://blog.csdn.net/u014074697/article/details/88800007

乐观锁

默认别人不会修改数据,所以不会上锁,只有在做更新的时候才会判断更新期间有没有其他人去更新这个数据,常用方法有版本号机制和CAS算法实现。乐观锁适用于多读少写的场景,可以提高吞吐量。像数据库提供的wirte_condition机制,其实都是提供的乐观锁机制。在java.util.concurrent.atomic包下的原子变量类型就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

我们知道,这两种锁各有优缺点,像乐观锁适用于多读少写的场景,即冲突发生的情况比较少,省去了锁的开销,提高系统的吞吐量。但如果写比较频繁,一般会经常发生冲突(写期间会有其他线程也做了写操作),这就导致了应用会不断的进行retry,这样反倒降低了性能。所以,写多的场景用悲观锁比较合适

乐观锁常用的两种实现方式

乐观锁一般采用版本号机制或CAS算法来实现

1.版本号机制

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

场景举例:

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

  • 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 ¥50,待更新余额:¥50=(100-50)。
  • 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20,待更余额¥80=(100-20)。
  • 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=¥50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  • 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

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

2.CAS算法

CAS(compare and swap-比较与交换),是一种有名的无锁算法。 即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking-Sychronization)。是基于硬件进行实现的,出现在java.util.concurrent.automic(额头妹课)包下,通过unsafe类的compareAndSwapInteger/Long/String/Double/Array..(Object expect,Object new);进行实现的,unsafe方法通过c++实现的。

CAS算法涉及三个操作数:

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

注意,只有当V==A时,才会执行V=B;比较和替换是原子性的,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

CAS自旋:在资源竞争激烈的情况下,如果CAS在判断两次读取内存的值不一样时(先从内存进行读取-预期值,然后创建一个新值用于替换内存值,替换前,将预期值和内存值进行比较,发现不一致)会放弃操作,但是通常会继续再次发起CAS操作,形成自旋几率较大,自旋很浪费CPU资源的。

 

乐观锁的缺点

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

1 ABA 问题

  1. 初次读取 V=A
  2. ......
  3.  更新检查V=A

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

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

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

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

CAS与synchronized的使用情景

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值