悲观锁
当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。
这种借助数据库锁机制在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。
之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
另外,还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
乐观锁
乐观锁( Optimistic Locking )
是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
悲观锁实现方式:
1.数据库方式:
如淘宝下单过程中扣减库存的需求说明一下如何使用悲观锁:
//0.开始事务 begin; //1.查询出商品库存信息 select quantity from items where id=1 for update; //2.修改商品库存为2 update items set quantity=2 where id = 1; //3.提交事务 commit;
在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。
上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。也就是说悲观锁不注意加索引的话会锁表。
2.代码方式:
- synchronized关键字和Lock的实现类都是悲观锁
乐观锁实现方式:
- 乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
1.版本号用于在数据库中
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。当需要对账户信息表进行更新的时候,需要首先读取version字段。
//修改商品库存
update item
set quantity=quantity +1
where id = 1 and quantity - 1 > 0
以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0
的方式进行乐观锁控制。
修改版本号
update data
set dataversion=dataversion+1
where id=1 and dataversion={dataversion}
以上SQL语句中,如果版本号为1,通过dataversion={dataversion}的方式进行乐观锁控制。
2.用于CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试
如何选择?
在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。
1、乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。因为乐观锁就是为了避免悲观锁的弊端出现的,所以适合应用在读为居多的场景下。
2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。因为悲观锁会影响系统吞吐的性能,所以适合应用在写为居多的场景下。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。
附:公平锁和⾮公平锁
1)什么是公平锁和⾮公平锁?
1.公平锁:是指多个线程按照申请锁的顺序来获取锁。
2.⾮公平锁:并不是按照申请锁的顺序,有可能后申请的线程⽐先申请的线程优先获取到锁。在⾼并发的情况下,有可能造成优先级反转或者饥饿现象。
3.注意: synchronized 和 ReentrantLock 默认是⾮公平锁 。
非公平锁能更充分的利⽤CPU的时间,尽量减少CPU空闲状态时间,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得⾮常⼤了(因为阻塞线程恢复需要点时间),变相的也就减少了线程切换的开销
如果为了更⾼的吞吐量,很显然⾮公平锁是⽐较合适的,因为节省很多线程切换时间,吞吐量就上去了。除此之外那就公平锁,大家公平使。
可重入锁(又名递归锁)
概念:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞
优点:所以Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是:在一定程度避免死锁。
死锁及排查
死锁是指两个或两个以上的线程在执⾏过程中,因争夺资源⽽造成的⼀种互相等待的现象,若⽆外⼒⼲涉那它们都将⽆法推进下去
利用JREjconsole可以检测出当前进程中的死锁进程。
什么是自旋锁?
1.是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
2.当线程如果发现锁被占用时,会不断循环判断锁的状态,直到获取。
好处是减少线程上下文切换的消耗,
缺点是循环会消耗CPU。
2)CAS缺点
1.如果循环时间长,开销很大。
2.引出来ABA问题。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。