简述自旋锁与互斥锁的使用场景
# 自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU
/**
* 面试题分享:手写一个自旋锁
*
**/
public class SpinLockDemo {
AtomicReference<Thread> lock = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
//如果不为空,自旋
while (!lock.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
//解锁后,将锁置为 null
lock.compareAndSet(thread,null);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 自旋锁优缺点
缺点:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU 使用率极高。
- 上面 Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
优点:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是 active 的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
# 自旋锁的使用场景
如果锁的使用者持有锁的时间非常长,那么其余的等待获得锁的线程就会一直做自旋操作,这是非常浪费 CPU 的使得 CPU 有效利用率大大降低。其次如果并发抢锁的线程非常多,也会加大这种浪费。
那么显然锁的持有时间非常短是使用自旋锁的场景。
因为自旋锁不会阻塞不会陷入内核重新调度其他线程,所以如果自旋带来的 CPU 消耗比阻塞,陷入内核,OS 调度其他线程,再切回用户态这一系列过程来的 CPU 消耗少那么自旋锁就能够节省 CPU,让 CPU 更多的去执行有效代码! 因为线程切换,陷入内核等等这一系列的操作和傻傻的自旋操作一样都是在做“无意义”事情,白白的消耗 CPU。
# 互斥锁
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。
当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。
所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。
性能开销的成本就是上下文切换的成本。
# 自旋锁和互斥锁的使用场景
a. 如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁 b. 如果临界区需要睡眠,应该选择互斥锁 c. 中断里应该使用自旋锁,因为中断处理中不允许睡眠
编辑 (opens new window)
上次更新: 2023/02/17, 17:03:51