【锁】详解区分 互斥锁、⾃旋锁、读写锁、乐观锁、悲观锁

今天看了下常见的几种锁:

互斥锁、⾃旋锁、读写锁、乐观锁、悲观锁,总结一下


互斥锁和自旋锁

最底层的就是互斥锁自旋锁,有很多⾼级的锁都是基于它们实现的

加锁的⽬的就是保证共享资源在任意时间⾥,只有⼀个线程访问,这样就可以避免多线程导致共享数据错乱的问题

互斥锁和⾃旋锁的区别就是对于加锁失败后的处理⽅式是不⼀样的:

  • 互斥锁加锁失败后,线程会释放CPU,给其他线程。加锁的代码就会被阻塞。
  • 自旋锁加锁失败后,线程会忙等待,也就是一直请求加锁,直到它拿到锁

也就是当加锁失败时,互斥锁⽤「线程切换」来应对,⾃旋锁则⽤「忙等待」来应对。


如图

image-20211027180207331

所以,互斥锁加锁失败,会从用户态陷入到内核态,让内核帮我们切换线程,这会有两次线程上下文切换的成本,具有一定的性能开销:

  • 当线程加锁失败时,内核会把线程的状态从运行状态设置为睡眠状态,然后把CPU切换给其他线程运行
  • 当锁被释放时,之前睡眠状态的线程会变为就绪状态,然后内核会在合适时间,把CPU切换给该线程运行

因为虚拟内存是共享的,这里上下文切换的是线程私有数据、寄存器等不共享的数据

所以如果代码运行时间很短,可以考虑不用互斥锁,而是选用自旋锁


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

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

第⼀步,查看锁的状态,如果锁是空闲的,则执⾏第⼆步;

第⼆步,将锁设置为当前线程持有;

CAS函数就把这两步骤合并成一条硬件级指令,形成原子指令


读写锁

读写锁读锁写锁两部分构成,如果只读共享资源用读锁加锁,如果需要修改共享资源则用写锁加锁

工作原理

写锁没有被线程持有时,多个线程可以并发地持有读锁,但是当写锁被线程持有后,其他线程获取读锁写锁的操作都会阻塞

读写锁在读多写少的场景,能发挥出优势


根据实现的不同分为读优先锁写优先锁

读优先锁

image-20211027191104656

写优先锁

image-20211027191202357

但是这两种都会造成线程“饥饿”的问题,比如

读优先锁:一直有读线程获取读锁,那么写线程将永远获取不到,造成写线程“饥饿”。

写优先锁:如果⼀直有写线程获取写锁,读线程也会被「饿死」。


所以我们可以搞一个公平读写锁

公平读写锁⽐较简单的⼀种⽅式是:⽤队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。


乐观锁 悲观锁

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

前⾯提到的互斥锁、⾃旋锁、读写锁,都是属于悲观锁。

乐观锁:假定冲突的概率很低,它的⼯作⽅式是:先修改完共享资源,再验证这段时间内有没有发⽣冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。另外虽然叫锁,但是乐观锁全程并没有加锁,所以它也叫⽆锁编程。

一般会使用版本号机制CAS算法实现





posted @ 2021-10-27 19:32  CJ-cooper  阅读(510)  评论(0编辑  收藏  举报