您的位置:首页 > 科技 > IT业 > 【多线程】常见的锁策略

【多线程】常见的锁策略

2024/11/16 4:25:30 来源:https://blog.csdn.net/acm_pn/article/details/142307225  浏览:    关键词:【多线程】常见的锁策略

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


        我们之前详细分析过java标准库中的synchronized锁,事实上,这只是各种锁实现方式中的一种,不同的语言锁的实现策略都是有所不同的,包括java中也有除synchronized以外别的锁。在后续开发中我们也可以根据实际需求,结合一些常见锁策略的特点来自己实现一个合适的锁。接下来就让我们在这篇文章中好好分析一下一些常见的锁策略吧。

一、乐观锁与悲观锁

悲观锁:
加锁的时候假设出现锁冲突的概率很大,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁, 这样别⼈想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
加锁的时候假设出现锁冲突的概率不大,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让⽤⼾决定如何去做。
举个栗⼦: 同学 A 和 同学 B 想请教⽼师⼀个问题.
  • 同学 A 认为 "⽼师是⽐较忙的, 我来问问题, ⽼师不⼀定有空解答"。因此同学 A 会先给⽼师发消息: "⽼师你忙嘛? 我下午两点能来找你问个问题嘛?" (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题。如果得到了否定的答复, 那就等⼀段时间, 下次再来和⽼师确定时间。这个是悲观锁。
  • 同学 B 认为 "⽼师是⽐较闲的, 我来问问题, ⽼师⼤概率是有空解答的"。因此同学 B 直接就来找⽼师.(没加锁, 直接访问资源) 如果⽼师确实⽐较闲, 那么直接问题就解决了。如果⽼师这会确实很忙, 那么同学 B也不会打扰⽼师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突)。这个是乐观锁.
这两种思路不能说谁优谁劣,⽽是看当前的场景是否合适.
如果当前⽼师确实⽐较忙,那么使⽤悲观锁的策略更合适,使⽤乐观锁会导致 "⽩跑很多趟",耗费额外的资源。
如果当前⽼师确实⽐较闲,那么使⽤乐观锁的策略更合适,使⽤悲观锁会让效率⽐较低。
注意:
java中synchronized这把锁采取的是一种 “自适应”的加锁方式,初始情况下使用 乐观锁策略,同时会记录锁冲突发生的次数,当发现锁竞争比较激烈,即 锁冲突次数达到一定程度的时候,就会自动切换成 悲观锁策略

二、重量级锁与轻量级锁

锁的核⼼特性 "原⼦性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
  • CPU 提供了 "原⼦操作指令"。
  • 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁。
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized ReentrantLock 等关键字和类。

注意, synchronized 并不仅仅是对 mutex 进⾏封装, 在 synchronized 内部还做了很多其他的⼯作
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
  • ⼤量的内核态⽤⼾态切换
  • 很容易引发线程的调度
轻量级锁: 加锁机制尽可能不使⽤ mutex,⽽是尽量在⽤户态代码完成。实在搞不定了,再使⽤ mutex
  • 少量的内核态⽤⼾态切换.
  • 不太容易引发线程调度
注意:
乐悲观锁是站在 “预估锁冲突”的角度,而重量轻量锁则是站在 “加锁开销”的角度,这两个策略还是有本质区别的。
同样,java中的synchronized锁也是 “自适应”的,开始时是一个轻量锁,如果发现锁冲突比较严重也会切换为重量锁。

三、挂起等待锁与自旋锁

按之前的⽅式,线程在抢锁失败后进⼊阻塞状态,放弃 CPU,需要过很久才能再次被调度。
但实际上,⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU。这个时候就可以使⽤⾃旋锁来处理这样的问题。
⾃旋锁(Spin Lock)伪代码:
while ( 抢锁 (lock) == 失败 ) {}
如果获取锁失败,⽴即再尝试获取锁,⽆限循环,直到获取到锁为⽌。第⼀次获取锁失败,第⼆次的尝试会在极短的时间内到来。
⼀旦锁被其他线程释放,就能第⼀时间获取到锁。
理解⾃旋锁 vs 挂起等待锁
想象⼀下,去追求⼀个⼥神。当男⽣向⼥神表⽩后,⼥神说:你是个好⼈,但是我有男朋友了~~
  • 挂起等待锁:陷⼊沉沦不能⾃拔.... 过了很久很久之后,突然⼥神发来消息,“咱俩要不试试?” (注意,这个很⻓的时间间隔⾥,⼥神可能已经换了好⼏个男朋友了)。
  • ⾃旋锁:死⽪赖脸坚韧不拔。仍然每天持续的和⼥神说早安晚安,⼀旦⼥神和上⼀任分⼿,那么就能⽴刻抓住机会上位。
⾃旋锁是⼀种典型的 轻量级锁/乐观锁 的实现⽅式(JVM内部用户态代码实现的)。
  • 优点:没有放弃 CPU,不涉及线程阻塞和调度,⼀旦锁被释放,就能第⼀时间获取到锁。
  • 缺点:如果锁被其他线程持有的时间⽐较久,那么就会持续的消耗 CPU 资源。(⽽挂起等待的时候是不消耗 CPU 的)。

挂起等待锁是典型的 重量级锁/悲观锁 的实现方式(调用操作系统api在内核中实现的)。

  • 优点:会放弃CPU资源的竞争,不会消耗额外的资源,可以让CPU专心做其他事
  • 缺点:重新恢复锁竞争的时机不可控,可能要过很长时间才能获取到锁
synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的。

四、公平锁与非公平锁

假设三个线程 A、B、C。 A 先尝试获取锁,获取成功。然后 B 再尝试获取锁,获取失败,阻塞等待,然后 C也尝试获取锁,C 也获取失败,也阻塞等待。
当线程 A 释放锁的时候,会发⽣啥呢?
  • 公平锁:遵守“先来后到”。B ⽐ C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。
  • ⾮公平锁不遵守“先来后到”。B 和 C 都有可能获取到锁。
这就好⽐⼀群男⽣追同⼀个⼥神。当⼥神和前任分⼿之后,先来追⼥神的男⽣上位,这就是公平锁;如果是⼥神不按先后顺序挑⼀个⾃⼰看的顺眼的,就是⾮公平锁

公平锁:

非公平锁:

注意:
  • 操作系统内部的线程调度就可以视为是随机的。如果不做任何额外的限制,锁就是⾮公平锁。如果要想实现公平锁,就需要依赖额外的数据结构如队列等,来记录线程加锁的先后顺序
  • 公平锁和⾮公平锁没有好坏之分,关键还是看适⽤场景
java中的synchronized就属于非公平锁。

五、可重入锁与不可重入锁

可重⼊锁的字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。
⽐如⼀个递归函数⾥有加锁操作,递归过程中这个锁会阻塞⾃⼰吗?如果不会,那么这个锁就是可重⼊锁(因为这个原因可重⼊锁也叫做递归锁)。
如果一个线程,针对一把锁连续加锁两次,就可能出现死锁,而把锁设定成“可重入”就可以避免死锁了,具体相关概念在博主的 深入剖析线程安全问题 一文中做过详细的叙述。
可重入锁的特点:
  • 记录当前持有锁的线程的信息
  • 在加锁时判定,当前申请锁的线程是否为锁持有者的线程
  • 计数器,记录加锁的次数,从而确定何时真正释放锁
Java⾥只要以Reentrant开头命名的锁都是可重⼊,⽽且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重⼊的。⽽ Linux 系统提供的 mutex 是不可重⼊锁

六、读写锁

多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
读写锁(readers-writer lock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,重复读者之间并不互斥,⽽写者则要求与任何⼈互斥。
⼀个线程对于数据的访问,主要存在两种操作:读数据写数据.
  • 两个线程都只是读⼀个数据,此时并没有线程安全问题。直接并发的读取即可
  • 两个线程都要写⼀个数据,有线程安全问题
  • ⼀个线程读另外⼀个线程写,也有线程安全问题

由于在实际开发中,读操作出现频率是远远高于写操作的,而大多数时候的读操作并不会有线程安全问题,读写锁就是把读操作和写操作区分对待,避免不必要的阻塞等待,提高运行效率。Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁:

  • ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁。这个对象提供了 lock / unlock ⽅法进⾏加锁解锁
  • ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁。这个对象也提供了 lock / unlock ⽅法进⾏加锁解锁
其中:
  • 读加锁和读加锁之间,不互斥
  • 写加锁和写加锁之间,互斥
  • 读加锁和写加锁之间,互斥

总结

本文主要介绍了六种常见的锁策略,其中由以上论述可知,java中的synchronized的特点是:
  1. 乐观/悲观,自适应
  2. 重量锁/轻量锁,自适应
  3. 自旋锁/挂起等待锁,自适应
  4. 非公平锁
  5. 可重入锁
  6. 不是读写锁

这些锁策略是通用的,不光是java,其它语言也是适用的。以后如果要自己实现一个锁的话也可以参考这些锁策略来适当编写。


那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com