悲观锁
在每一个数据被修改前认为会影响其他事务,持保守意见,对数据进行加锁,这就是悲观锁(Pessimistic Concurrency Control)。
悲观锁主要分为共享锁(shared lock)和排他锁(exclusive locks)。
事务正在查询的数据加共享锁后,其他事务也能对该数据添加共享锁,对该条数据进行并发访问,但是不允许进行修改操作。
事务对准备修改的数据添加排他锁后,其他事务不能够对该条数据再添加任何锁(包括排他锁)。排他锁运行该事务对数据进行修改,其他事务不允许访问修改。
事务隔离级别中读已提交(Read committed)级别解决脏读问题可以用悲观锁实现。
悲观锁的实现依靠的是数据库底层的锁机制,只有依靠数据库底层的锁机制才能保证数据操作的排他性
由于悲观锁采取的保守策略,先加锁再处理的原则,数据库在操作过程中会造成很多没必要的开销,有可能还会产生死锁,其他事务只有等上一个事务完成后才能执行,解决并发问题的效率很低。
乐观锁
积极地认为数据操作之间不会有冲突,在数据操作完成后才会对数据进行冲突检测,有冲突就返回错误信息让用户自己选择如何处理,没有冲突就完成操作。
JDK1.8之后的concurrentHashMap采用的CAS就是乐观锁的一种实现。
实现乐观锁的方式
版本号控制
在表中加上一个version字段。
事务A读取张三账户此时为100,version为1。
事务B也读取张三账户为100,然后取钱取走了50,提交事务。张三账户余额此时为50,version+1=2,更新后的版本号比当前的版本号大,所以事务提交成功。
事务A此时也对张三账户取钱取走50,提交事务,更新后版本号为2,没有比当前版本号大,事务提交失败。解决第二类丢失更新的问题。
时间戳对比
在表中加上一个time字段。
事务A读取张三账户此时为100,时间戳为2020.01.01 12:00。
事务B也读取张三账户为100,时间戳为2020.01.01 12:00。然后取钱取走了50,提交事务。张三账户余额此时为50,当前时间戳和提交前获得的时间戳相同,所以事务提交成功,更新时间戳为2020.01.01 12:00。
事务A此时也对张三账户取钱取走50,提交事务,当前时间戳与提交前的时间戳进行对比,发现不相同,事务提交失败。
乐观锁主要可以解决第二类丢失更新的问题,第二类丢失问题就是事务A正在修改,事务B修改提交,事务A提交覆盖了事务B的数据。
乐观锁的实现不是基于数据库底层的锁机制,可以用于读多写少的场景,提高吞吐量。
间隙锁
在Mysql的可重复读 (RR)隔离级别中,可能会存在幻读的问题。例如在事务A正在查询id>0 and id < 10范围的数据,但是0-10里没有id = 2的行数据,这就是间隙(gap)。事务B此时插入一条id=2的数据,事务A两次查询出来的数据条数不一致,这就是幻读。
在RR隔离级别中,Mysql通过间隙锁+行锁(next-key lock)来解决幻读问题。
事务A开启事务,查询 id0-10范围的数据,此时事务B开启事务insert id = 2 的数据就会被阻塞,直到事务A提交事务。
间隙锁也可能会造成死锁,因为间隙锁不像排他锁,其他事务同样可以拿到。
死锁
多个线程之间相互争夺资源形成僵局的情况。
自旋锁
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会阻塞。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
可重入锁
允许线程一个获得对象锁之后,该线程再次访问不需要抢锁。
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
如果是非可重入锁的话,会导致死锁问题。此时一个带锁的x()方法,里面调用了带锁的y()方法,因为线程已经获得了对象锁,没办法再次获得对象锁,所以会出现死锁的情况。
总结,可重入锁的优点就是在以上情况避免了死锁。
实现原理
实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。
如果同一个线程再次请求这个锁,计数器将递增。
每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
公平锁和非公平锁
公平锁:线程A获得锁,线程B来抢锁,发现锁被占了,于是进入等待队列,线程C、D同样的情况,此时线程A释放掉了锁,通知线程B,线程B拿到锁,其他线程继续等待。
非公平锁:线程A释放掉锁,线程E此时来抢锁,是可以直接抢到锁的,不需要排队。
区别:非公平锁可以插队,我们必须先知道,在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。非公平锁在一定程度上效率比公平锁高。