各种锁
并发资源争抢就会涉及到锁。锁的种类有很多,如果不分类容易搞混。
以下涉及公平锁/非公平锁、乐观锁/悲观锁、独享锁/共享锁、互斥锁、读写锁、可重入锁(递归锁)/不可重入锁、自旋锁、自适应锁、偏向锁(意向锁)/轻量级锁/重量级锁、分段锁。
根据锁的性质可以分为:
- 公平锁/非公平锁
根据是否按照申请的先后顺序获得锁区分 - 乐观锁/悲观锁
根据认为获取的数据被修改的可能性区分 - 独享锁/共享锁
根据获取锁后,其他实例是否还能获取同样的锁来区分
实现方式:- 互斥锁:独享锁的实现
- 读写锁:有读状态和写状态。读状态时,仍可申请读锁,为共享锁;写状态时,不可申请其他锁,为独享锁。
读状态时,碰到写锁申请的请求会阻塞后续读锁的申请。
- 可重入锁(递归锁)/不可重入锁
根据获取锁的实例能否再次获取该锁区分
根据锁的涉及方案可以分为:
- 自旋锁
获取不到锁的时候循环获取锁,直到获取到锁。可以减少线程切换。 - 自适应锁
最近刚获得过锁意味着更容易成功获得锁,则增加自旋次数。
如果很少成功获取,则减少自旋次数。因为增加自旋次数能获取成功的机率也很低,并且自旋次数越大,浪费的 CPU 越多。 - 意向锁(偏向锁)/轻量级锁/重量级锁
会根据争抢激烈程度,逐渐升级。意向锁 -> 轻量级锁 -> 重量级锁。- 意向锁:对更细粒度加锁之前,先对路径加意向锁,避免其他实例获取锁的判断需要遍历所有数据。
- 轻量级锁:当另一个事务要进入争抢的时候,意向锁升级为轻量级锁,通过自旋的方式等待,线程不阻塞。
- 重量级锁:当自旋一定次数后,线程会被阻塞,进行线程切换(消耗资源大),轻量级锁升级为重量级锁。
- 分段锁
Hash 结构每条冲突链设置一个锁,减小锁影响的粒度。
重量级的加锁操作伴随着用户态到内核态切换、进程上下文切换等高消耗过程。
悲观锁/乐观锁
悲观锁假设获取的数据会被其他事务修改,所以读取时加锁以防止其他事务修改。如果其他事务需要修改数据,则需要等待悲观锁的释放。
乐观锁假设获取的数据不会被其他事务修改,所以读取时不加锁。更新时判断数据和读取时的数据是否一致,如果一致则将当前数据写入,否则等待该条件得到满足(自旋锁)或者驳回(版本号)。
从应用场景来看,悲观锁用于由于写多导致容易产生数据冲突的地方,以及不接受数据发生变化的情况。乐观锁用于读多写少不容易产生数据冲突的地方,提高吞吐量。
悲观锁举例:InnoDB 的共享锁和排他锁。
排他锁(写锁)
客户端如果获得不到锁,就进入睡眠状态,等待锁释放时的唤醒。
共享锁(读锁、独占锁)
一个事务获取共享锁之后,其他事务也可以获取共享锁。
互斥锁
互斥锁会导致获取不到锁的线程被挂起。
自旋锁
线程如果获得不到锁,就 自己循环 直到获得到锁,被挂起的几率低。
优势:
- 减少线程被挂起的几率
- 效率高
劣势:
- CPU 消耗高
要求:
- 锁竞争不激烈
- 锁占用时间短
其他种类:
- 阻塞锁
- 可重入锁
轻量级锁
CAS (compare and swap)实现
乐观锁的实现
仅在写入时可能需要等待。
- 版本号
如果不一致,则驳回或者合并(类似于 Git 解决冲突) - CAS (compare and swap)原子操作
假设有三种数据:待更新数据 A,事务开始时读取的数据 B,事务修改数据 B 得到 C。如果 A = B,则将 A 改为 C。
如果不相等,则进入循环等待,直到相等。
CAS 会出现 ABA 问题,即数据虽然与之前一致,但已发生过变化。并非所有场景都对该问题敏感,根据情况可以忽略该问题。
四种锁状态
- 无锁
- 意向锁:只有一个线程执行同步块
- 轻量级锁:线程交替执行同步块
- 重量级锁:依赖操作系统的 Mutex Lock
锁升级:由于锁竞争,升级锁。
锁升级的单向过程:意向锁 -> 轻量级锁 -> 重量级锁
公平锁/非公平锁
有优先级的锁为公平锁,反之为非公平锁
参考
mysql锁机制 乐观锁&悲观锁,共享锁&排他锁&意向锁&间隙锁
https://blog.csdn.net/xushiyu1996818/article/details/105558662
互斥锁(排它锁、独占锁、写锁、X锁)和共享锁(读锁、S锁) 自旋锁
https://my.oschina.net/u/2307114/blog/908009
java中synchronized的底层实现
https://www.jianshu.com/p/c97227e592e1
深入理解各种锁
https://www.jianshu.com/p/5725db8f07dc
漫画|Linux 并发、竞态、互斥锁、自旋锁、信号量都是什么鬼?
https://zhuanlan.zhihu.com/p/57354304
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
https://www.cnblogs.com/paddix/p/5405678.html
Let’s Talk Locks!
https://www.infoq.com/presentations/go-locks/
Java并发问题–乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
https://www.cnblogs.com/qjjazry/p/6581568.html
独占锁(写锁) / 共享锁(读锁) / 互斥锁
https://www.cnblogs.com/bbgs-xc/p/12791913.html
如何理解互斥锁、条件锁、读写锁以及自旋锁?
https://www.zhihu.com/question/66733477/answer/246535792
InnoDB 三种锁算法
- Record Lock
- Gap Lock
- Next-Key Lock = Gap Lock + Record Lock
Record Lock
唯一索引,粒度最细
Gap Lock
间隙锁用于非唯一索引。在可重复读的隔离级别中,用于防止其他事务插入数据导致的幻读。
使用 SELECT ... FOR UPDATE
或者 SELECT ... LOCK IN SHARE MODE
。
对于有序数字 1, 5, 10 ,它们之间都有一个范围可以存放其他数字。在这个空白的范围内称之为间隙。
在加排他锁时:
- 如果目标只有一个值:
- 如果目标是第一个索引值,则锁住无穷小到第一个索引值的范围
- 如果目标是最后一个索引值,则锁住最后一个索引值到无穷大的范围
- 否则锁住目标和前一个与目标值不相同的索引之间的范围
- 如果目标是范围,则锁住范围起止两个索引值之间的范围。
例1:
WHERE num BETWEEN 1 AND 5
,会锁住 (1, 5) 这个范围。
例2:
WHERE num = 5
,会锁住 (1, 5) 这个范围。
Next-Key Lock
对于间隙锁,如果范围中的右侧不是无限大,则同时锁住右侧的记录。
(1, 5]
与之相对的是 Previous-Key Lock,会锁住左侧的记录。
[1, 5)
对于范围查询,会直接使用范围。例如 > 2 ,则是 (2, +∞)