乐观锁和悲观锁的整理

385 阅读4分钟

前言

让时光积累出真正的价值

在数据库、Java程序当中我们都遇到过乐观锁和悲观锁这个概念。基于知识体系的完备性,需要好好了解一下乐观锁和悲观锁。

乐观锁与悲观锁又什么本质区别

乐观锁:使用乐观锁的时候一般是不注重过程的原子性,只在乎结果的有效性(允许多个一起修改变量的值,只要我的目的能实现就好了)。一般用在线程争抢不激烈的情况下,本质上是一种无锁机制。java提供的的util.concurrent.atomic包就有一套乐观锁的解决方案。

悲观锁:这个就是跟常用的Synchronized,ReentrantLock,思想就是拥有锁就独占式整个操作权限

使用乐观锁和悲观锁

两种锁的使用场景

乐观锁适用于多读少写的情况,这样能够节省锁的开销,以为乐观锁的冲突点主要发送再写入变量的时候。对应的悲观锁的使用场景就是发生在写入比较频繁的情况下。

乐观锁的实现机制:

乐观锁主要有两种实现机制:

  1. CAS操作:一种常见的实现方式,在修改数据之前获取数据,在修改数据之前判断一下数据是否跟之前的获取到的数据是否一致。如果一致证明之前没有被其他事务修改过,CAS操作成功,否则CAS操作失败。
  2. 版本号机制:在对象内部维护一个版本号,每次修改变量时都对版本号自增,且对比一下版本含和刚刚的版本号有没有区别,如果有区别证明变量被其他线程修改过了。没有则修改成功,给变量版本自增。java集合的快速失败机制就是靠这个思想实现的。
乐观锁怎么使用的

Java在util包里面提供了Atomic包。里面包含了一系列的操作包,他们是用CAS操作实现的。他们本质上是调用sun提供的unsafe类的CAC操作API来实现的。我们也自行调用unsafe类来实现CAS操作。

unsafe类的构造方法是私有的,而且直接实例话也会抛出异常。可以使用反射的方式获取到unfase类里面的静态实例常量。

AtomicStampedReference :这个类就是结合了版本号和CAS操作实现的原子引用类,可以解决ABA问题。

乐观锁的问题
ABA问题

乐观锁的思想就是先获取要修改的数据状态A,把数据修改为状态B的时候把数据为状态A作为修改为B的前提条件。

举个例子:有用户下单,需要对库存数进行扣除。使用CAS来做操作,先获取当前的库存数量,假设当前库存为100。在处理往其他以库存相关的业务之后扣减库存,需要将库存量设置为99,调用CAS操作设置库存为99的同时以当前库存量为100 为修改成功的前提条件。操作系统发现当前的库存为100,判断修改成功。

可是这里有一个问题,如果在我业务(主线程)开始获取库存量(获取到状态A)的之后其他线程将库存设置为99(状态B)然后再设置库存为100(状态A),在业务的最后修改库存的时候判断库存为100(发现状态还是A),就认为库存没有被修改过了,CAS操作成功!可是明明数据就是被修改过的(数据从状态A修改为状态B然后又修改为A)。

这就是ABA问题。如果多线程情况下变量被改为A然后再改为B,那CAS操作不就会认为线程没被操作过。

在Java1.5以后jdk提供了一个AtomicStampedReference原子引用类,在判断CAS操作是否成功的同时,还会附加判断版本号(version)是否被修改。

空自旋问题

如果争抢比较频繁多个线程就会在无休止的争抢中空耗。在争抢严重的时候不适合使用乐观锁。

悲观锁的改进

悲观锁缺点就是切换线程、堵塞等操作太消耗操作系统资源了。在Java1.6的基础上对synchronized进行了很大的有化,提出了偏向锁、轻量级锁、自旋锁等概念。使得使用synchronized在竞争不激烈的情况下性能接近乐观锁,在竞争激烈的情况下性能大大优于乐观锁。

最后

作为一个菜鸟,有很多地方理解不到位,欢迎指正