Java Web 实战 11 - 多线程进阶之常见的锁策略

大家好 , 这篇文章给大家带来的是多线程中常见的锁策略 , 我们会给大家讲解 6 种类别的锁

  1. 乐观锁 VS 悲观锁
  2. 普通的互斥锁 VS 读写锁
  3. 重量级锁 VS 轻量级锁
  4. 自旋锁 VS 挂起等待锁
  5. 公平锁 VS 非公平锁
  6. 可重入锁 vs 不可重入锁
  7. 常见面试题

针对常见的锁策略 , 这就属于面试爱考但是工作用不上的内容了
大家理解记忆即可
推荐大家跳转到这观看效果更佳
上一篇文章的链接我也给大家贴到这里了

小狗说要请大家喝饮料 , 快跟我说 : “谢谢小狗”
在这里插入图片描述
在这里插入图片描述

常见的锁策略

锁策略指的是 : 加锁的时候我们咋加的
这里说的锁策略 , 和语言基本无关 , 其他的语言也涉及到 “锁策略”

1. 乐观锁 VS 悲观锁

乐观锁 : 预测接下来锁冲突的概率不大 , 就需要做一类操作
悲观锁 : 预测接下来锁冲突的概率很大, 就需要做另一类操作

举个栗子 :
疫情导致封校了
有的人就比较紧张 , 去超市抓紧屯点零食 -> 乐观锁
有的人就无所谓 , 饿不死就行 -> 悲观锁
针对预测的结果不同 , 解决的方法不同

乐观锁相对来说 , 成本更小 ; 悲观锁相对来说 , 成本更大

我们之前介绍过 synchronized , 它既是悲观锁 , 又是乐观锁 , 也就是自适应锁
当前锁冲突概率不大 , 以乐观锁的方式运行 , 往往是纯用户态执行的
一旦发现锁冲突大了 , 以悲观锁的方式运行 , 往往要进入内核 , 对当前线程进行挂起等待

2. 普通的互斥锁 VS 读写锁

synchronized 就是普通的互斥锁 , 两个加锁操作会产生竞争

读写锁把加锁操作进行细化了 , 分成了 “加读锁” 和 “加写锁”
情况1 :
线程 A 尝试加写锁
线程 B 尝试加写锁
线程 A 和 线程 B 产生竞争 , 和普通的锁没啥区别
情况 2 :
线程 A 尝试加读锁
线程 B 尝试加读锁
线程 A 和 线程 B 不产生竞争 , 锁相当于没加一样
(多线程读 , 不涉及修改 , 是线程安全的)
情况 3 :
线程 A 尝试加读锁
线程 B 尝试加写锁
线程 A 和线程 B 产生竞争 , 和普通的锁没什么区别

只有情况 2 (两个线程都是加读锁) 是线程安全的

读写锁就是把读操作和写操作区分对待 , Java 标准库提供了 ReentrantReadWriteLock类 , 实现了读写
锁.

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁 . 这个对象提供了 lock / unlock方法进行加锁解锁 .
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁 . 这个对象也提供了 lock / unlock方法进行加锁解锁 .

3. 重量级锁 VS 轻量级锁

重量级锁指的是锁的开销比较大 , 做的工作比较多
轻量级锁指的是锁的开销比较小 , 做的工作比较少

悲观锁 , 经常会是重量级锁
乐观锁 , 经常会是轻量级锁
但是不是绝对的

那什么叫做 做的工作多 , 做的工作少呢 ?
我们一般认为 , 锁这个东西要保持互斥的 , 那保持互斥要有力量来源的
我们 Java 中实现一把锁 , 需要使用 synchronized 关键字 (后续还会降解 ReentrantLock)
那 Java 里面实现锁 , 主要是 JVM 提供的 synchronized 和 ReentrantLock 这两个机制
那 JVM 之所以能够实现锁机制 , 是因为操作系统提供了 mutex 互斥锁
操作系统之所以能够加锁 , 是因为 CPU 提供了一些用来加锁的 , 能够保证原子操作的指令
image.png
重量级锁主要是依赖了操作系统提供的锁 , 使用操作系统提供的锁 , 就很容易产生阻塞等待
轻量级锁主要是尽量的避免使用操作系统提供的锁 , 尽量在用户态完成功能 , 也就是尽量的避免用户态和内核态的切换 , 尽量避免挂起等待 (阻塞等待)

synchronized 是自适应锁 , 既是轻量级锁 , 又是重量级锁
也是根据锁冲突的情况来决定的
冲突的不高就是轻量级锁 , 冲突的很高就是重量级锁

4. 自旋锁 VS 挂起等待锁

自旋锁和挂起等待锁是更加深入来看的 , 相当于分析的最内层

自旋锁 : 当我们发现锁冲突的时候 , 不会挂起等待 , 它会迅速再次尝试这个锁能不能获取到 (超级舔狗)
他就相当于是一个 while 循环一直获取锁的状态
一旦锁被释放 , 就可以第一时间获取到
如果锁一直不释放 , 就会消耗大量的 CPU
自旋锁是更加轻量的 , 效率更高的

挂起等待锁 : 发现锁冲突 , 就挂起等待
一旦锁被释放 , 不能第一时间获取到
在锁被其他线程占用的时候 , 会放弃 CPU 资源
挂起等待锁是更加重量的 , 效率更低的

自旋锁 , 是轻量级锁的具体实现
挂起等待锁 , 是重量级锁的具体实现

自旋锁是轻量级锁 , 也是乐观锁
挂起等待锁是重量级锁 , 也是悲观锁

synchronized 作为轻量级锁的时候 , 内部是自旋锁
synchronized 作为重量级锁的时候 , 内部是挂起等待锁

5. 公平锁 VS 非公平锁

啥样的情况才算是公平呢 ?
符合 “先来后到” 这样的规则 , 就是公平

先来的 , 先排在前面 . 来晚的 , 就在后面排着

举个栗子 :
image.png
操作系统中 , 默认的锁的竞争规则 , 就是非公平的 , 没有考虑先来后到
如果要想使用公平锁 , 就需要使用额外的数据结构来进行控制实现
所以 synchronized 是非公平锁

6. 可重入锁 vs 不可重入锁

image.png

7. 常见面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上面的图).

  1. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中.

  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.

  1. synchronized 是可重入锁么?

是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

到这里本篇文章就结束了
如果有帮助的话请一键三连~
小狗会请你喝饮料的
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_53117341/article/details/129529232