线程同步互斥锁和读写锁的区别和各自适用场景

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程同步互斥锁和读写锁的区别和各自适用场景相关的知识,希望对你有一定的参考价值。

参考技术A 读写锁特点:

1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁特点:

一次只能一个线程拥有互斥锁,其他线程只有等待

自旋锁:

一次只能有一个进程进入临界区,读写锁是自旋锁的一个特例。

应用场景:

以队列操作为例:

线程A对队列负责将数据写入队列。须采取“互斥锁”或“读写锁的写锁”

线程B队列负责从队列读出数据。须采取“互斥锁”或“读写锁的写锁”,读队列操作,不可采取“读写锁的读锁”,因为从队列读出数据时,需要更改队列本身的下标索引,如果多个线程同时操作该队列的话,就会导致队列下标索引混乱。
参考技术B 奴隶制儇焚ww

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

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

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


互斥锁和自旋锁

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

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

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

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

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


如图

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

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

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

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


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

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

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

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

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


读写锁

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

工作原理

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

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


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

读优先锁

写优先锁

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

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

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


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

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


乐观锁 悲观锁

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

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

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

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





以上是关于线程同步互斥锁和读写锁的区别和各自适用场景的主要内容,如果未能解决你的问题,请参考以下文章

信号量,互斥锁,读写锁和条件变量的区别

自旋锁和互斥锁的区别

多线程的同步和互斥有啥区别

互斥锁,自旋锁,原子操作原理和实现

互斥锁的示例

悲观锁和乐观锁