# 自旋锁与互斥锁的性能对比及适用场景
面试官问"自旋锁和互斥锁有什么区别,什么时候用哪个",大部分人能说出"自旋锁忙等待,互斥锁会睡眠",但追问"性能拐点在哪里""单核为什么不能用自旋锁""自旋锁会不会活锁"就答不上来了。这道题的核心是理解:选择哪种锁取决于临界区执行时间和上下文切换开销的比较。
# 简要回答
自旋锁在用户态死循环检测锁状态(忙等待),拿到锁的瞬间延迟极低,但等待期间 CPU 一直在空转。互斥锁通过系统调用让拿不到锁的线程睡眠,CPU 可以去干别的事,但唤醒线程需要用户态→内核态→用户态两次切换,延迟高。判断标准很简单:如果临界区的执行时间短于两次上下文切换的开销(通常 1-10μs),用自旋锁划算;否则用互斥锁。
# 详细回答
自旋锁怎么工作的
自旋锁的核心是一个原子变量(比如 std::atomic_flag)。加锁时用 test_and_set 原子操作尝试把标志从 0 改成 1,如果失败说明别人持有锁,就在一个 while 循环里不断重试。解锁时把标志清零。整个过程不涉及系统调用,不会让线程睡眠,所以没有上下文切换开销。代价是等待期间 CPU 核心被完全占用,什么有用的事都干不了。
互斥锁怎么工作的
互斥锁(std::mutex)加锁失败时,会通过系统调用(Linux 上是 futex)把当前线程放到等待队列里并让它睡眠。持有锁的线程解锁时,内核会唤醒等待队列里的一个线程。这个过程涉及用户态到内核态的切换(保存/恢复寄存器、切换页表等),开销大约 1-10μs。好处是睡眠期间 CPU 核心可以执行其他线程。
性能拐点在哪里
如果临界区只做一两次内存读写(几十纳秒),自旋锁的忙等待开销远小于互斥锁的两次上下文切换开销,自旋锁性能更好。如果临界区要做 IO 操作、复杂计算(几十微秒以上),自旋锁让等待线程白白烧 CPU,互斥锁让线程睡眠反而更高效。经验值:临界区执行时间超过 10-20μs 就该用互斥锁。
单核为什么不能用自旋锁
单核 CPU 上只有一个执行流。如果线程 A 持有锁,线程 B 开始自旋等待,但 B 占着唯一的 CPU 核心不放,A 根本没机会运行来释放锁——形成类似死锁的状态。只有在可抢占内核(时间片到了强制切换)或者 B 主动 yield() 让出 CPU 的情况下,单核才能勉强用自旋锁,但这时候自旋锁的优势已经没了。

# 知识拓展
Q:自旋锁会不会活锁?
会。多个线程同时自旋竞争同一把锁时,可能出现"惊群效应"——所有线程都在自旋但谁也拿不到锁。解决办法:引入随机退避(每次失败后随机等待一小段时间再重试)、使用票据锁(ticket lock)保证公平性、或者用 MCS 锁减少缓存行争用。
Q:如何确定自旋次数上限?
实际工程中常用"自适应自旋"策略:先自旋一定次数(比如上下文切换开销的 1.5-2 倍),如果还拿不到锁就退化成互斥锁让线程睡眠。Java 的 synchronized 和 Linux 内核的 mutex_lock 都用了这种混合策略。
Q:C++ 标准库有自旋锁吗?
没有专门的自旋锁类,但可以用 std::atomic_flag 的 test_and_set + clear 自己实现。C++20 新增了 std::atomic_flag::wait() 和 notify_one(),可以实现更高效的等待。实际项目中如果需要自旋锁,通常用平台相关的实现(如 Linux 的 pthread_spinlock_t)。
Q:读写锁和自旋锁/互斥锁是什么关系?
读写锁是更高层的抽象——多个读者可以同时持有锁,但写者独占。读写锁的底层实现可以基于自旋锁(读多写少+短临界区场景)或互斥锁(长临界区场景)。C++ 标准库的 std::shared_mutex 底层通常基于互斥锁实现。
评论
验证登录状态...