在 Linux 内核与高性能服务端开发面试中,同步原语的选择几乎是必考题。面试官不仅想知道你“会不会用”,更想考察你是否理解每种锁背后的代价模型,以及在不同竞争强度、临界区长度和读写比例下如何做出合理决策。本文围绕自旋锁、互斥锁、读写锁与无锁编程,梳理它们的核心差异与选型逻辑。
一、先理解锁的两个核心成本
选择锁的本质,是在两种成本之间做权衡:
- 阻塞成本:线程拿不到锁时进入睡眠、上下文切换、唤醒的代价。通常在微秒级甚至更高。
- 自旋成本:线程拿不到锁时忙等待,持续占用 CPU 的代价。在临界区极短时,自旋可能比睡眠更划算。
一个经验法则:如果临界区执行时间小于两次上下文切换的时间,自旋更优;否则阻塞更优。 这条法则贯穿所有锁的选型。
二、自旋锁(spinlock)
自旋锁在获取失败时不会睡眠,而是循环检测锁状态。它适用于:
- 临界区极短,通常只有几条指令,例如修改一个计数器、操作一个链表指针。
- 中断上下文或不可睡眠的上下文。内核中断处理程序不能睡眠,只能使用自旋锁。
- 多核环境。单核上自旋没有意义,因为持有者无法并行推进。
使用自旋锁的注意事项:
- 持有自旋锁期间不能睡眠,否则可能死锁。
- 需要关闭抢占或中断(如
spin_lock_irqsave),防止中断处理程序在同一 CPU 上再次获取同一把锁。 - 竞争激烈时,自旋会浪费大量 CPU,此时应改用互斥锁。
面试中常问:自旋锁和互斥锁的区别? 标准回答是:自旋锁忙等待、不睡眠、适合短临界区和中断上下文;互斥锁会睡眠、适合长临界区和进程上下文。
三、互斥锁(mutex)
互斥锁在获取失败时会让出 CPU,进入睡眠,直到锁可用时被唤醒。它适用于:
- 临界区较长,可能涉及内存分配、文件 I/O、复杂计算。
- 进程上下文,允许睡眠的场景。
- 竞争激烈时,避免大量 CPU 空转。
互斥锁的代价是上下文切换。如果临界区只有几十纳秒,却发生了一次睡眠唤醒,整体开销反而远大于自旋。因此在高频短临界区场景,Linux 内核和用户态库常采用自适应锁:先自旋一小段时间,若仍未获取再睡眠。例如 glibc 的 pthread_mutex 和内核的 mutex 都有类似优化。
四、读写锁(rwlock)
读写锁区分读锁和写锁:多个读者可并发持有读锁,写者独占。它适合读多写少的场景,例如配置表、路由表、缓存元数据。
但读写锁并非银弹,需要注意:
- 写者饥饿:读者源源不断时,写者可能长期拿不到锁。部分实现采用写者优先策略缓解。
- 读锁开销并不低:读写锁内部通常有原子计数和自旋逻辑,纯读场景下可能比无锁方案更慢。
- 升级/降级问题:多数读写锁不支持原子地从读锁升级为写锁,强行升级容易死锁。
如果读操作极其频繁且临界区极短,可以考虑 RCU(Read-Copy-Update)。RCU 让读者几乎零开销,写者通过复制修改再替换指针,延迟释放旧数据。Linux 内核大量使用 RCU 管理路由表、文件描述符表等。
五、无锁编程(lock-free)
无锁编程不依赖锁,而是通过原子操作(CAS、fetch-add 等)和内存屏障保证一致性。常见结构包括无锁队列、无锁栈、原子计数器。
无锁的优势:
- 避免死锁和优先级反转。
- 在低到中等竞争下吞吐量高。
- 对中断和信号处理友好。
无锁的代价与陷阱:
- ABA 问题:CAS 时值从 A 变 B 再变回 A,导致误判。可用带版本号的指针或
LL/SC指令缓解。 - 内存回收困难:节点被移除后,可能仍有读者持有引用,需要 RCU、 Hazard Pointer 或引用计数。
- 内存序复杂:需要正确使用
acquire/release语义,否则在弱内存模型架构(如 ARM)上出现难以复现的 bug。 - 高竞争下退化:大量 CAS 失败重试会浪费 CPU,此时锁反而更稳定。
因此,无锁编程适合数据结构简单、竞争可控、对延迟敏感的场景,而不是盲目替代所有锁。
六、选型决策树
面试时可按以下顺序回答:
- 能否不用共享状态? 能则用线程本地存储或消息传递,彻底避免同步。
- 临界区是否极短且不可睡眠? 是则选自旋锁。
- 读操作是否远多于写操作? 是则考虑读写锁或 RCU。
- 竞争是否激烈、临界区是否较长? 是则选互斥锁。
- 是否对延迟极度敏感且数据结构简单? 是则考虑无锁编程。
- 是否读多写少且写操作可延迟? 优先 RCU。
七、常见面试追问
- 自旋锁能用在单核上吗? 可以,但必须关闭抢占,否则持有者被抢占后无人释放,自旋者永远等待。
- 读写锁一定比互斥锁快吗? 不一定。读锁也有原子操作开销,写多或临界区极短时互斥锁可能更快。
- 无锁一定比有锁快吗? 不一定。低竞争下无锁有优势,高竞争下 CAS 重试可能导致性能反而不如互斥锁。
- RCU 和读写锁的区别? RCU 读者几乎无开销且不阻塞写者,但写者需要处理延迟释放和宽限期,适合读极多写极少的场景。
总结
锁的选择没有绝对答案,核心是理解竞争强度、临界区长度、读写比例、是否可睡眠这四个维度。自旋锁赢在短临界区和不可睡眠上下文,互斥锁赢在长临界区和高竞争,读写锁和 RCU 赢在读多写少,无锁编程赢在简单结构和低延迟。面试中展示出这种权衡思维,比背出某个锁的定义更能打动面试官。
未经允许不得转载:任鹏个人博客 » Linux 面试题:自旋锁、互斥锁、读写锁与无锁编程怎么选

