各种锁

各种锁🔒

当有多个线程同时访问共享资源时,该共享资源可能会数据错乱,此时,锁就有了作用。

加锁的目的是为了保证共享资源在任意时间里,只有一个线程访问,这样可以避免多线程导致共享数据错乱的问题。

互斥锁

  • 当线程加锁成功后,该线程会占用该共享资源,直到该线程解锁,这期间其他线程无法访问该共享资源
  • 当互斥锁加锁失败后,线程会释放CPU,把资源给其他线程,进入阻塞状态

互斥锁加锁失败后,会从用户态陷入内核态,让内核来切换线程,这存在一定的性能开销

  • 加锁失败,内核把线程的状态从【运行】->【阻塞】状态,把资源让给洽谈线程
  • 当锁被释放后,该线程有机会拿到该锁。从【阻塞】状态转为【就绪】状态

自旋锁

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

一般加锁的过程,包含两个步骤:

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

CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。

比如,设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID,那么 CAS(lock, 0, pid) 就表示自旋锁的加锁操作,CAS(lock, pid, 0) 则表示解锁操作。

加锁失败后,一直忙等待,直到获取锁

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

读写锁

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

乐观锁和悲观锁

悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁

乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作

其中读写锁、互斥锁、自旋锁都是悲观锁

实际上,我们常见的git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。

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


各种锁
http://example.com/2023/11/05/各种锁/
作者
Forrest
发布于
2023年11月5日
许可协议