目录

各种锁

并发资源争抢就会涉及到锁。锁的种类有很多,如果不分类容易搞混。

以下涉及公平锁/非公平锁、乐观锁/悲观锁、独享锁/共享锁、互斥锁、读写锁、可重入锁(递归锁)/不可重入锁、自旋锁、自适应锁、偏向锁(意向锁)/轻量级锁/重量级锁、分段锁。

根据锁的性质可以分为:

  • 公平锁/非公平锁
    根据是否按照申请的先后顺序获得锁区分
  • 乐观锁/悲观锁
    根据认为获取的数据被修改的可能性区分
  • 独享锁/共享锁
    根据获取锁后,其他实例是否还能获取同样的锁来区分
    实现方式:
    • 互斥锁:独享锁的实现
    • 读写锁:有读状态和写状态。读状态时,仍可申请读锁,为共享锁;写状态时,不可申请其他锁,为独享锁。
      读状态时,碰到写锁申请的请求会阻塞后续读锁的申请。
  • 可重入锁(递归锁)/不可重入锁
    根据获取锁的实例能否再次获取该锁区分

根据锁的涉及方案可以分为:

  • 自旋锁
    获取不到锁的时候循环获取锁,直到获取到锁。可以减少线程切换。
  • 自适应锁
    最近刚获得过锁意味着更容易成功获得锁,则增加自旋次数。
    如果很少成功获取,则减少自旋次数。因为增加自旋次数能获取成功的机率也很低,并且自旋次数越大,浪费的 CPU 越多。
  • 意向锁(偏向锁)/轻量级锁/重量级锁
    会根据争抢激烈程度,逐渐升级。意向锁 -> 轻量级锁 -> 重量级锁。
    • 意向锁:对更细粒度加锁之前,先对路径加意向锁,避免其他实例获取锁的判断需要遍历所有数据。
    • 轻量级锁:当另一个事务要进入争抢的时候,意向锁升级为轻量级锁,通过自旋的方式等待,线程不阻塞。
    • 重量级锁:当自旋一定次数后,线程会被阻塞,进行线程切换(消耗资源大),轻量级锁升级为重量级锁。
  • 分段锁
    Hash 结构每条冲突链设置一个锁,减小锁影响的粒度。

重量级的加锁操作伴随着用户态到内核态切换、进程上下文切换等高消耗过程。

悲观锁/乐观锁

悲观锁假设获取的数据会被其他事务修改,所以读取时加锁以防止其他事务修改。如果其他事务需要修改数据,则需要等待悲观锁的释放。

乐观锁假设获取的数据不会被其他事务修改,所以读取时不加锁。更新时判断数据和读取时的数据是否一致,如果一致则将当前数据写入,否则等待该条件得到满足(自旋锁)或者驳回(版本号)。

从应用场景来看,悲观锁用于由于写多导致容易产生数据冲突的地方,以及不接受数据发生变化的情况。乐观锁用于读多写少不容易产生数据冲突的地方,提高吞吐量。

悲观锁举例:InnoDB 的共享锁和排他锁。

排他锁(写锁)

客户端如果获得不到锁,就进入睡眠状态,等待锁释放时的唤醒。

共享锁(读锁、独占锁)

一个事务获取共享锁之后,其他事务也可以获取共享锁。

互斥锁

互斥锁会导致获取不到锁的线程被挂起。

自旋锁

线程如果获得不到锁,就 自己循环 直到获得到锁,被挂起的几率低。

优势:

  • 减少线程被挂起的几率
  • 效率高

劣势:

  • CPU 消耗高

要求:

  • 锁竞争不激烈
  • 锁占用时间短

其他种类:

  • 阻塞锁
  • 可重入锁

轻量级锁

CAS (compare and swap)实现

乐观锁的实现

仅在写入时可能需要等待。

  1. 版本号
    如果不一致,则驳回或者合并(类似于 Git 解决冲突)
  2. CAS (compare and swap)原子操作
    假设有三种数据:待更新数据 A,事务开始时读取的数据 B,事务修改数据 B 得到 C。如果 A = B,则将 A 改为 C。
    如果不相等,则进入循环等待,直到相等。
    CAS 会出现 ABA 问题,即数据虽然与之前一致,但已发生过变化。并非所有场景都对该问题敏感,根据情况可以忽略该问题。

四种锁状态

  • 无锁
  • 意向锁:只有一个线程执行同步块
  • 轻量级锁:线程交替执行同步块
  • 重量级锁:依赖操作系统的 Mutex Lock

锁升级:由于锁竞争,升级锁。
锁升级的单向过程:意向锁 -> 轻量级锁 -> 重量级锁

公平锁/非公平锁

有优先级的锁为公平锁,反之为非公平锁

参考

java锁的种类
https://www.jianshu.com/p/7e3cf7469c83

mysql锁机制 乐观锁&悲观锁,共享锁&排他锁&意向锁&间隙锁
https://blog.csdn.net/xushiyu1996818/article/details/105558662
互斥锁(排它锁、独占锁、写锁、X锁)和共享锁(读锁、S锁) 自旋锁
https://my.oschina.net/u/2307114/blog/908009

https://upload-images.jianshu.io/upload_images/4491294-e3bcefb2bacea224.png?imageMogr2/auto-orient/strip|imageView2/2/format/webp

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, +∞)

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html