互斥锁、⾃旋锁、读写锁、可重入锁、乐观锁、悲观锁

1、互斥锁和自旋锁

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

1.1、互斥锁

使用场景: 如果你能确定被锁住的代码执行时间很长,就应该用互斥锁。

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

互斥锁加锁失败后,线程会释放 CPU ,给其他线程,自身处于获取锁阻塞状态,然后从用户态切换到内核态由由内核帮助进行切换线程,当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行,着之间的过程会产生上下文切换。

  • 获取线程失败的线程会从Running(运行态) - > Sleep(睡眠态),然后把CPU切换给其他线程运行。
  • 当锁被释放后,线程会从Sleep(睡眠态) -> Ready(就绪)状态,然后内核把CPU切换给该进程。

1.2、自旋锁

使用场景: 如果被锁住的代码执行时间很短,就使用自旋锁

自旋锁是通过CPU函数在用户态完成加锁和解锁操作,不产生上下文切换, 但是自旋锁会产生忙等待,自旋的线程会占用消耗CPU资源。此种加锁方式只适合于分时系统,不能工作在单CPU的硬件。

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

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

  • 互斥锁加锁失败后,线程会释放CPU,给其他线程。加锁的代码就会被阻塞。

  • 自旋锁加锁失败后,线程会忙等待,也就是一直请求加锁,直到它拿到锁,也就是当加锁失败时,互斥锁⽤「线程切换」来应对,⾃旋锁则⽤「忙等待」来应对。

2、读写锁

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

当写锁没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。

但是,一旦写锁被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。

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

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

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

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

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

3、可重入锁

可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁。

例如,一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法【即可重入】,而无需重新获得锁;对于不同线程则相当于普通的互斥锁。

4、乐观锁、悲观锁

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁

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

乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:

先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

作者:joker.liu  创建时间:2023-04-20 10:20
最后编辑:joker.liu  更新时间:2023-04-21 14:33