🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇
线程安全之锁策略
🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇
今日推荐歌曲: 达尔文 -- 林俊杰 🎵🎵
系列文章目录
3. 什么是⾃旋锁 , 为什么要使⽤⾃旋锁策略呢,缺点是什么?
前言
锁策略不仅仅是局限于Java. 任何和"锁"相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的. 这类问题是面试官很喜欢问的一类题, 考验一个程序员的功底, 这里主要介绍常用的几大锁策略.
一. 乐观锁 VS 悲观锁 🔒
两种不同的锁的实现方式
1. 乐观锁
在加锁之前, 预估当前出现锁冲突的概率不大, 因此在进行加锁的时候就不会做太多的工作.
加锁过程做的事情比较少, 加锁的速度可能就更快,但是更容易引入一些其他的问题 ( 但是可能会消耗更多的cpu资源 ) , 可能会去反复加锁, 消耗更多的 cpu 资源
2. 悲观锁
在加锁之前,预估当前锁冲突出现的概率比较大,因此加锁的时候,就会做更多的工作.
做的事情更多, 加锁的速度可能更慢, 但是整个过程中不容易出现其他问题~~
举个栗子:
同学A和同学B想请教⽼师⼀个问题.
同学A认为 "⽼师是⽐较忙的,我来问问题,⽼师不⼀定有空解答". 因此同学A会先给⽼师发消息:"⽼师 你忙嘛?我下午两点能来找你问个问题嘛?"(相当于加锁操作)得到肯定的答复之后,才会真的来问问题. 如果得到了否定的答复,那就等⼀段时间,下次再来和⽼师确定时间.这个是悲观锁.
同学B认为"⽼师是⽐较闲的,我来问问题,⽼师⼤概率是有空解答的".因此同学B直接就来找⽼师.(没 加锁,直接访问资源)如果⽼师确实⽐较闲,那么直接问题就解决了.如果⽼师这会确实很忙,那么同学B 也不会打扰⽼师,就下次再来(虽然没加锁,但是能识别出数据访问冲突).这个是乐观锁.
这两种思路不能说谁优谁劣,⽽是看当前的场景是否合适. 如果当前⽼师确实⽐较忙,那么使⽤悲观锁的策略更合适,使⽤乐观锁会导致"⽩跑很多趟",耗费额外 的资源. 如果当前⽼师确实⽐较闲,那么使⽤乐观锁的策略更合适,使⽤悲观锁会让效率⽐较低.
二. 轻量级锁 VS 重量级锁 🔒
1. 概念
- 轻量级锁,加锁的开销小,加锁的速度更快.=>轻量级锁,一般就是乐观锁
- 重量级锁,加锁的开销更大,加锁速度更慢.=>重量级锁,一般也就是悲观锁
2. 轻重量级与悲乐观锁的区别
轻量重量 , 加锁之后 , 对结果的评价.
悲观乐观 , 是加锁之前 , 对未发生的事情进行的预估整体来说 , 这两种角度 , 一个是已经发生的结果的评价 , 一个是未发生的预估 , 描述的是同一个事情
三. 自旋锁 和 挂起等待锁 🔒
1. 自旋锁 (Spin Lock)
按之前的⽅式,线程在抢锁失败后进⼊阻塞状态,放弃CPU,需要过很久才能再次被调度. 但实际上,⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃CPU.这 个时候就可以使⽤⾃旋锁来处理这样的问题.
旋锁伪代码:
1 while (抢锁(lock) == 失败) {}
如果获取锁失败,⽴即再尝试获取锁,⽆限循环,直到获取到锁为⽌.第⼀次获取锁失败,第⼆次的尝试会 在极短的时间内到来. ⼀旦锁被其他线程释放,就能第⼀时间获取到锁
⾃旋锁是⼀种典型的轻量级锁的实现⽅式.
进行加锁的时候,搭配一个 while 循环 .如果加锁成功, 自然循环结束.
如果加锁不成功,不是阻塞放弃 cpu ,而是进行下一次循环,再次尝试获取到锁.这个反复快速执行的过程,就称为 “自旋" 一旦其他线程释放了锁,就能第一时间拿到锁 同时,这样的自旋锁, 也是乐观锁. 使用自旋的前提, 就是预期锁冲突概率不大,其他线程释放了锁,就能第一时间拿到.
2. 挂起等待锁
挂起等待锁就是重量级锁的一种典型实现 , 同时也是一种悲观锁
进行挂起等待的时候 , 就需要内核调度器介入 , 这一块要完成的操作就多了.真正获取到锁要花的时间也就更多一些了. 这个锁可以适用于锁冲突激烈的情况
举个栗子:
想象一下, 去追求一个女神. 当男生向女神表白后,女神说:你是个好人,但是我有男朋友了~~
挂起等待锁:陷入沉沦不能自拔...过了很久很久之后,突然女神发来消息, "咱俩要不试试?"(注意,这个很长的时间间隔里,女神可能已经换了好几个男票了).
自旋锁:死皮赖脸坚韧不拔.仍然每天持续的和女神说早安晚安.一旦女神和上一任分手,那么就能立刻抓住机会上位.
四. 普通互斥锁 和 读写锁 🔒
1. 普通互斥锁
很经典的互斥锁 , synchronized 的加锁和解锁
2. 读写锁
多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都 需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
读写锁(readers-writerlock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,复数读者之间并不互斥,⽽写者则要求与任何⼈互斥。
⼀个线程对于数据的访问,主要存在两种操作:读数据 和 写数据.
- 读锁和读锁之间,不会出现锁冲突(不会阻塞)
- 写锁和写锁之间,会出现锁冲突(会阻塞)
- 读锁和写锁之间,会出现锁冲突(会阻塞)
读写锁就是把读操作和写操作区分对待.Java标准库提供了 ReentrantReadWriteLock 类,实现了读写锁.
注意,只要是涉及到"互斥",就会产⽣线程的挂起等待.⼀旦线程挂起,再次被唤醒就不知道隔了多久 了. 因此尽可能减少"互斥"的机会,就是提⾼效率的重要途径.
读写锁特别适合于"频繁读,不频繁写"的场景中.(这样的场景其实也是⾮常⼴泛存在的).
比如: 老师给学生布置作业, 布置一次作业(写一次) ,全班那么多同学, 要批阅数十次作业(读数十次)
所以建议少布置点作业 ! !
五. 公平锁 VS 非公平锁🔒
假设三个线程A,B,C.A先尝试获取锁,获取成功.然后B再尝试获取锁,获取失败,阻塞等待;然后C 也尝试获取锁,C也获取失败,也阻塞等待. 当线程A释放锁的时候,会发⽣啥呢?
公平锁 : 遵守"先来后到".B⽐C先来的.当A释放锁的之后,B就能先于C获取到锁.
⾮公平锁 : 不遵守"先来后到".B和C都有可能获取到锁.
注意:
• 操作系统内部的 线程调度就可以视为是随机的.如果 不做任何额外的限制,锁 就是⾮公平锁.如果要 想实现公平锁 ,就需要 依赖额外的数据结构 ,来记录线程们的先后顺序. (比如用个队列)
• 公平锁和⾮公平锁没有好坏之分 , 关键还是看适⽤场景 (synchronized 是⾮公平锁.)
六. 可重入锁 VS 不可重入锁🔒
1. 可重入锁
可重⼊锁的字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。 ⽐如⼀个递归函数⾥有加锁操作,递归过程中这个锁会阻塞⾃⼰吗?如果不会,那么这个锁就是可重 ⼊锁(因为这个原因可重⼊锁也叫做递归锁)。
Java⾥只要以 Reentrant 开头命名的锁都是可重⼊锁,⽽且JDK提供的所有现成的Lock实现类,包括 synchronized 关键字锁都是可重⼊的。.
2. 不可重入锁
Linux 系统提供的 mutex 是不可重⼊锁
理解"把⾃⼰锁死" ⼀个线程没有释放锁,然后⼜尝试再次加锁.
按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第⼆个 锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进⾏解 锁操作.这时候就会死锁.
七. 相关⾯试题 ⭐⭐⭐
1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤,会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤.并不会真的加锁,⽽是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突. 悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex ), 获取到锁再操作数据.获取不到锁就等待. 乐观锁的实现可以引⼊⼀个版本号.借助版本号识别出当前的数据访问是否冲突.(实现细节参考上⾯).
2. 介绍下读写锁?
读写锁就是把读操作和写操作分别进⾏加锁. 读锁和读锁之间不互斥. 写锁和写锁之间互斥. 写锁和读锁之间互斥. 读写锁最主要⽤在"频繁读,不频繁写"的场景中.
3. 什么是⾃旋锁 , 为什么要使⽤⾃旋锁策略呢,缺点是什么?
如果获取锁失败,⽴即再尝试获取锁,⽆限循环,直到获取到锁为⽌.第⼀次获取锁失败,第⼆次的尝试 会在极短的时间内到来.⼀旦锁被其他线程释放,就能第⼀时间获取到锁. 相⽐于挂起等待锁, 优点:没有放弃CPU资源,⼀旦锁被释放就能第⼀时间获取到锁,更⾼效.在锁持有时间⽐较短的场景 下⾮常有⽤. 缺点:如果锁的持有时间较⻓,就会浪费CPU资源.
4. synchronized 是可重⼊锁么?
是可重⼊锁. 可重⼊锁指的就是连续两次加锁不会导致死锁. 实现的⽅式是在锁中记录该锁持有的线程⾝份,以及⼀个计数器(记录加锁次数).如果发现当前加锁的 线程就是持有锁的线程,则直接计数⾃增.
八. 总结
真嘟很细节, 半夜 手敲 原创 , /(ㄒoㄒ)/~~ 累死我了
希望能帮助大伙 , 有什么问题可以在评论区讨论哦 ~~ 博客不易,点赞 收藏 加关注,知识进脑不迷路!!!