乐观锁与悲观锁java
概述算法
乐观锁
老是假设最好的状况,每次去读数据的时候都认为别人不会修改,因此不会上锁, 可是在更新的时候会判断一下在此期间有没有其余线程更新该数据, 可使用版本号机制和CAS算法实现。 乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于write_condition机制,其实都是提供的乐观锁。 在Java中java.util.concurrent.atomic包下面的原子变量类就是基于CAS实现的乐观锁。数据库
悲观锁
老是假设最坏的状况,每次去读数据的时候都认为别人会修改,因此每次在读数据的时候都会上锁, 这样别人想读取数据就会阻塞直到它获取锁 (共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。 传统的关系型数据库里边就用到了不少悲观锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。 Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。并发
使用场景
乐观锁适用于写比较少的状况下(多读场景),即冲突真的不多发生的时候,这样能够省去了锁的开销,加大了系统的整个吞吐量。分布式
乐观锁适用于读比较少的状况下(多写场景),若是是多写的状况,通常会常常产生冲突,这就会致使上层应用会不断的进行retry,这样反却是下降了性能,因此通常多写的场景下用悲观锁就比较合适。高并发
乐观锁比如生活中乐观的人老是想着事情往好的方向发展,悲观锁比如生活中悲观的人老是想着事情往坏的方向发展。 这两种人各有优缺点,不能不以场景而定说一种人好于另一种人。性能
乐观锁常见的两种实现方式atom
版本控制
通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version++便可。 当线程A要更新数据值时,在读取数据的同时也会读取version值, 在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新, 不然重试更新操做,直到更新成功。线程
举个例子:版本控制
假设数据库中账户信息表中有一个 version 字段,而且 version=1;而当前账户余额字段(balance)为 $100 。
操做员 A 此时将其读出 (version=1),并从其账户余额中扣除 $50($100-$50)。
操做员 A 操做的同事,操做员B 也读入此用户信息(version=1),并从其账户余额中扣除 $20($100-$20)。
操做员 A 完成了修改工做,version++(version=2),连同账户扣除后余额(balance=$50),提交至数据库更新,
此时因为提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
操做员 B 完成了操做,也将版本号version++(version=2)试图向数据库提交数据(balance=$80),
但此时比对数据库记录版本时发现,操做员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,
不知足提交版本必须大于记录当前版本才能执行更新的乐观锁策略,所以,操做员 B 的提交被驳回。
避免了操做员 B 用基于 version=1 的旧数据修改的结果覆盖操做员A 的操做结果的可能。
CAS算法
硬件支持的原子性操做最典型的是:比较并交换(Compare-and-Swap,CAS)。 CAS 指令须要有 3 个操做数,分别是内存地址 V、旧的预期值 A 和新值 B。 当执行操做时,只有当 V 的值等于 A,才将 V 的值更新为 B。
//著名的CAS
//var1是比较值所属的对象,var2须要比较的值(但实际是使用地址偏移量来实现的),
//若是var1对象中偏移量为var2处的值等于var4,那么将该处的值设置为var5并返回true,若是不等于var4则返回false。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
乐观锁的缺点
1.ABA问题
若是一个变量初次读取的时候是 A 值,它的值被改为了 B,后来又被改回为 A,那 CAS 操做就会误认为它历来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题, 它能够经过控制变量值的版原本保证 CAS 的正确性。 大部分状况下 ABA 问题不会影响程序并发的正确性, 若是须要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
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的使用情景
对于资源竞争较少(线程冲突较轻)的状况, 使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操做额外浪费消耗cpu资源; 而CAS基于硬件实现,不须要进入内核,不须要切换线程,操做自旋概率较少,所以能够得到更高的性能。
对于资源竞争严重(线程冲突严重)的状况,CAS自旋的几率会比较大, 从而浪费更多的CPU资源,效率低于synchronized。
免费Java高级资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。