互斥锁、⾃旋锁、读写锁、可重入锁、乐观锁、悲观锁
1、互斥锁和自旋锁
最底层的就是互斥锁和自旋锁,有很多⾼级的锁都是基于它们实现的。
1.1、互斥锁
使用场景: 如果你能确定被锁住的代码执行时间很长,就应该用互斥锁。
加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。
互斥锁加锁失败后,线程会释放 CPU ,给其他线程,自身处于获取锁阻塞状态,然后从用户态切换到内核态由由内核帮助进行切换线程,当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行,着之间的过程会产生上下文切换。
- 获取线程失败的线程会从Running(运行态) - > Sleep(睡眠态),然后把CPU切换给其他线程运行。
- 当锁被释放后,线程会从Sleep(睡眠态) -> Ready(就绪)状态,然后内核把CPU切换给该进程。
1.2、自旋锁
使用场景: 如果被锁住的代码执行时间很短,就使用自旋锁
自旋锁是通过CPU函数在用户态完成加锁和解锁操作,不产生上下文切换, 但是自旋锁会产生忙等待,自旋的线程会占用消耗CPU资源。此种加锁方式只适合于分时系统,不能工作在单CPU的硬件。
- 第一步,查看锁的状态,如果锁空闲,执行第二步
- 第二步,将锁设置为当前线程所持有
互斥锁 和 ⾃旋锁的区别, 就是对于加锁失败后的处理⽅式是不⼀样的:
互斥锁加锁失败后,线程会释放CPU,给其他线程。加锁的代码就会被阻塞。
自旋锁加锁失败后,线程会忙等待,也就是一直请求加锁,直到它拿到锁,也就是当加锁失败时,互斥锁⽤「线程切换」来应对,⾃旋锁则⽤「忙等待」来应对。
2、读写锁
读写锁 适用于能明确区分读操作和写操作的场景
当写锁没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
但是,一旦写锁被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
根据实现的不同分为 读优先锁 和 写优先锁。
但是这两种都会造成线程“饥饿”的问题,比如
读优先锁:一直有读线程获取读锁,那么写线程将永远获取不到,造成写线程“饥饿”。
写优先锁:如果⼀直有写线程获取写锁,读线程也会被「饿死」。
公平读写锁 : 比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。
3、可重入锁
可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁。
例如,一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法【即可重入】,而无需重新获得锁;对于不同线程则相当于普通的互斥锁。
4、乐观锁、悲观锁
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:
先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
最后编辑:joker.liu 更新时间:2023-04-21 14:33