前言:
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生 活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以 场景而定说一种人好于另外一种人。
一:悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿 数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资 源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线 程)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁 等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
二:乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上 锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐 观锁。
在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了 乐观锁的一种实现方式 CAS 实现的。两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一 种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的 时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的 情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
三:乐观锁常见的两种实现方式
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 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库 提交数据(balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略, 因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
2.CAS算法
即 compare and swap(比较与交换),是一种有名的无锁算法。
无锁编程, 即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的 情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS 算法涉及到三个操作数
1.需要读写的内存值 V
2.进行比较的值 A
3.拟写入的新值 B 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则 不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点
1.ABA问题
如果一个变量 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 类把多个共享变量合并成一个共享变量来操作。