文章目录
- 前言
- 1. 乐观锁 vs 悲观锁
- 2. 轻量级锁 vs 重量级锁
- 3. 自旋锁 vs 挂起等待锁
- 4. 读写锁 vs 互斥锁
- 5. 公平锁 vs 非公平锁
- 6. 可重入锁 vs 不可重入锁
- 总结
前言
本章节所讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的.
本文中讲解的锁, 并不是指某个具体的锁, 而是一个抽象的概念, 描述的是 “一类锁”. 即使是普通的程序猿也需要了解一些, 对于合理的使用锁也是有很大帮助的.
关注收藏, 开始学习吧🧐
1. 乐观锁 vs 悲观锁
乐观锁:
预测该场景中, 不太会出现锁冲突的情况. 假设数据一般情况下不会产生并发冲突, 所以在数据进行提交更新的时候, 才会正式对数据是否产生并发冲突进行检测, 如果发现并发冲突了, 则让返回用户错误的信息, 让用户决定如何去做.
悲观锁:
预测该场景中, 非常容易出现锁冲突. 总是假设最坏的情况, 每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁, 这样别人想拿这个数据就会阻塞直到它拿到锁.
锁冲突: 指两个线程尝试去获取一把锁, 一个线程获取成功, 则另一个线程就会阻塞等待, 这就是锁冲突.
2. 轻量级锁 vs 重量级锁
锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
- CPU 提供了 “原子操作指令”.
- 操作系统基于 CPU 的原子指令, 实现了
mutex
互斥锁. - JVM 基于操作系统提供的互斥锁, 实现了
synchronized
和ReentrantLock
等关键字和类.
注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的 工作
轻量级锁:
加锁机制尽可能的不使用 mutex
, 而是尽量在用户态代码完成. 加锁开销比较小, 花费时间少, 占用资源较少.
重量级锁:
加锁机制重度依赖 OS 提供的 mutex
. 加锁开销比较大, 花费时间多, 占用资源较多.
注意:
- 一个乐观锁, 很可能是一把轻量级锁. 而一个悲观锁, 很可能是一把重量级锁. (并不绝对)
- 悲观乐观, 是在加锁之前, 对锁冲突概率的一个预测, 决定之后工作的多少. 而重量轻量, 是在加锁之后, 考量实际的锁的开销.
- 正是因为概念有些重合, 在针对某个具体的锁时, 可能把它叫做乐观锁, 也可能叫做轻量级锁.
3. 自旋锁 vs 挂起等待锁
挂起等待锁:
挂起等待锁, 是重量级锁的一种典型实现. 通过内核态, 借助系统提供的锁机制, 当出现锁冲突的时候, 会牵扯到内核对于线程的调度. 将冲突的线程挂起 (阻塞等待).
自旋锁:
自旋锁, 是轻量级锁的一种典型实现. 在用户态下, 通过自旋的方式 (while 循环), 实现类似于加锁的效果.
自旋锁伪代码:
while (抢锁(lock) == 失败) {}
- 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
- 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是
不消耗 CPU 的, 但无法第一时间获取到锁).
4. 读写锁 vs 互斥锁
多线程之间, 数据的读取方之间不会产生线程安全问题, 但数据的写入方互相之间以及和读者之间都需要进行互斥. 如果两种场景下都用同一个锁, 就会产生极大的性能损耗. 所以读写锁因此而产生.
读写锁 (readers-writer lock), 顾名思义, 在执行加锁操作时需要额外表明读写意图, 复数读者之间并不互斥, 而写者则要求与任何人互斥.
一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
- 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写一个数据, 有线程安全问题.
- 一个线程读, 另外一个线程写, 也会有线程安全问题.
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock
类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock
类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.ReentrantReadWriteLock.WriteLock
类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
其中,
- 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
而在实际开发中, 读操作出现的频率, 往往比写操作要高得多. 在该场景中, 使用读写锁的话, 就可以尽可能的避免产生锁竞争, 此时, 多线程并发执行的效率就会更高. 而如果使用互斥锁的话, 就会产生不必要的挂起等待, 这就是前者读写锁存在的意义.
5. 公平锁 vs 非公平锁
假设有三个线程 A, B, C.
A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待. 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发生什么呢?
公平锁:
遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁:
不遵守 “先来后到”. B 和 C 都有可能获取到锁. 谁先拿到锁就是谁的.
注意:
- 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
- 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
6. 可重入锁 vs 不可重入锁
可重入锁:
可重入锁的字面意思是 “可以重新进入的锁”, 即允许同一个线程多次获取同一把锁.
比如一个递归函数里有加锁操作, 递归过程中这个锁会阻塞自己吗? 如果不会, 那么这个锁就是可重入锁 (因为这个原因可重入锁也叫做递归锁).
不可重入锁:
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会产生死锁. 这个锁就是不可重入锁.
那么, 死锁具体是什么一个什么样的情况呢? 我们在下一篇博文详细讲述.
总结
✨ 本文主要讲述了锁的几个主要策略. 主要讲解了几大锁策略的概念, 以及其所对应的场景.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!