悲观锁和乐观锁,什么是悲观,什么是乐观?

悲观锁与乐观锁

上一篇在这
在前面对死锁问题的进行探索后,我们来看看悲观锁和乐观锁。

我们知道,多线程访问共享资源时为了避免资源竞争导致数据错乱的问题会在访问共享资源前进行加锁操作,最常用的就是互斥锁。互斥锁、自旋锁、读写锁都属于悲观锁,除了悲观锁之外还有乐观锁。


互斥锁与自旋锁

在之前的学习中(多线程互斥与同步的实现)我们已经接触到了互斥锁和自旋锁,现在进一步展开对它们的学习。

📗互斥锁和自旋锁是最底层的两种锁,其他的改机锁都是基于它们实现的。

我们知道,加锁的目的是保证共享资源在任意时间段内只能有一个线程进行访问,避免共享资源错乱的问题。

在已经有一个线程加锁后,其他线程的加锁操作就会失败,而在互斥锁和自旋锁中,两者对于加锁失败的处理方式是不一样的:

  • 互斥锁加锁失败后线程会释放CPU给其它线程
  • 自旋锁加锁失败后线程会处于“ 忙等待 ” 状态,直到它成功获取到锁。

互斥锁是一种“ 独占锁 ”,当有一个线程加锁成功后,互斥锁就已经被这个线程独占了,只要这个线程不释放锁,其它线程的加锁操作就会一直处于失败状态,并且会释放CPU并让给其它线程,变成阻塞状态。

互斥锁加锁失败而变为阻塞状态这一过程是由操作系统内核形成的。 加锁失败时内核会把线程置为“ 睡眠 ” 状态,等到锁被释放后寻找合适的时机将该线程唤醒,继续执行线程。

在这里插入图片描述

在这个过程中也存在一定的性能开销成本:两次线程上下文切换的成本

  • 加锁失败时内核会把线程的状态从运行状态设置为睡眠状态·,并把CPU切换给其它线程
  • 释放锁时将睡眠状态的线程唤醒变为就绪状态,内核在合适的时间将CPU切换给该线程运行

如果我们能够确定被锁住的代码执行时间很短,就可以选用自旋锁而不是互斥锁。因为线程上下文切换比代码执行时间还长的话系统性能很低。

自旋锁是通过CPU提供的CAS函数,在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,相比于互斥锁开销更小速度更快。

自旋锁的加锁步骤一般为两步:

  • 查看锁的状态,如果锁是空闲的则进行第二步
  • 将所设置为当前线程所持有

注意:CPU提供的函数CAS函数将这两个步骤合并成了一条硬件级指令,形成原子指令,不会产生两个步骤只执行一步的情况。

自旋锁是比较简单的一种锁,一直自旋,利用CPU周期,直到锁可用。在单核CPU上需要抢占式的调度器(不断通过时钟中断一个线程,运行其它线程),不然自旋锁在单核CPU上无法使用,因为一个自旋的线程永远不会放弃CPU。

⛵自旋锁与互斥锁在使用层面上相似,但在实现层面上完全不同:当加锁失败时互斥锁通过 “ 线程切换 ” 来实现,而自旋锁则通过 “ 忙等待 ” 来应对。

自旋锁开销小,在多核系统下一般不会产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长则自旋的线程会长时间占用CPU资源,所以自旋的时间和被锁住的代码执行的时间是成正比关系的。


读写锁

读写锁由 “ 读锁 ” 和 “ 写锁 ”两部分组成,读取共享资源通过读锁进行加锁操作,修改共享资源通过写锁进行加锁操作。

它既可以基于自旋锁实现,也可以由互斥锁实现。


💡读写锁适用于能够明确区分读操作和写操作的场景。

读写锁的原理:

  • 当 “ 写锁 ” 没有被线程持有时,多个线程能够并发的持有 “ 读锁 ” 。 这大大提高了共享资源的访问效率,因为 ” 读锁 “ 是用于读取共享资源的场景,不会对数据进行修改,所以多个线程同时持有读锁不会破坏共享资源的数据。
  • 在有线程持有“ 写锁 ” 后,其它所有读线程的获取读锁操作就会被阻塞。

所以,“ 读锁 ” 是共享锁,而 “ 写锁 ” 是独占锁 ,读写锁在读多写少的场景能够发挥出优势。

并且,读写锁根据实现的不同可以分为 “ 读优先锁 ” 和 “ 写优先锁 ”。

顾名思义,读优先锁就是读锁能够被更多的线程持有,以便提高线程的并发性,当一个线程先持有了读锁,写线程在获取写锁时会被阻塞,并在阻塞过程中后续的读线程依然可以获取读锁,只有在所有读线程释放了读锁后写线程才能够获取些锁。

在这里插入图片描述

写优先锁则是优先服务写线程,当读线程先持有了读锁,写线程在获取写锁时会被阻塞,后来的读线程再获取读锁时,不能成功获取,会进入阻塞状态,只有在写线程成功获取写锁并操作完成释放锁后,其它线程才能获取锁。

在这里插入图片描述

  • 读优先锁对于读线程并发性更好,但也存在问题,若是一直有读线程获取读锁那么则写线程就一直不能获取写锁,出现 饥饿 现象,无法运行写线程。
  • 写线程也同样存在相同的问题

🚦所以,为了解决这两个问题引入了 “ 公平读写锁 ” ,公平读写锁的一种简单实现方式是用队列把获取锁的线程排队,不管是读线程还是写线程都按照先进先出的原则进行加锁操作。这样读线程依旧可以并发,也不会·出现后续的 饥饿 现象。


悲观锁和乐观锁

上面提到的互斥锁、自旋锁、读写锁都属于悲观锁,它们有一个共同特点:多线程同时修改共享资源的概率较高,很容易出现冲突,所以在访问共享资源之前都会先上锁。

所以,与之对应的就是乐观锁了。

乐观锁则是先修改完共享资源,再验证这段时间内是否有其它线程与之产生了冲突,如果有其它线程也修改了共享资源则放弃此次操作,若是没有则操作完成。

乐观锁全程都没有加锁,因此也叫无锁编程。

📖乐观锁使用场景:在线文档、Git

在线文档就是一个乐观锁的具体使用场景,在线文档可以同时有很多进行编辑,在编辑完成之后再进行验证修改的内容是否有冲突,有冲突则编辑失败,没有冲突则编辑成功。

我们在使用Git进行代码编辑提交时也是同样的过程,先对代码进行编辑,在提交后服务器进行冲突判断。

🔥乐观锁虽然去除了加锁操作,但是一旦发生冲突解决问题的成本很大,所以只有在冲突概率非常低并且加锁成本非常高的场景下才考虑使用乐观锁。


以上就是本篇关于悲观锁和乐观锁的内容了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值