死锁与同步经典问题

共 20 题
📑 题目列表 20 题
#
★★★

1. 死锁产生的四个必要条件是什么?预防、避免、检测与解除四类策略分别破坏哪个条件?

死锁产生的四个必要条件是什么?预防、避免、检测与解除四类策略分别破坏哪个条件?

  • 四个必要条件:互斥、占有且等待、不可剥夺、循环等待。
  • 预防:破坏四个条件之一(如一次性申请全部资源、可剥夺、资源排序)。
  • 避免:银行家算法,在分配前判断是否安全。

死锁产生的四个必要条件是:互斥(资源一次只能被一个进程使用)、占有且等待(进程持有资源又在等待其他资源)、不可剥夺(资源不能被强行夺走)、循环等待(存在一个进程等待链形成环)。四者同时满足才可能死锁。预防策略设法破坏其一:破坏互斥(用假脱机/可共享资源)、破坏占有且等待(要求进程一次性申请全部所需资源)、破坏不可剥夺(允许剥夺)、破坏循环等待(对所有资源规定顺序,进程按升序申请)。避免策略(如银行家算法)在每次分配前动态判断系统是否仍处于安全状态,不满足则拒绝分配,避免进入不安全状态。检测策略定期检查资源分配图是否成环,一旦成环即判定死锁;解除策略则通过终止部分进程(撤销其资源)或剥夺资源分配给其他进程来打破死锁。

四类策略的切入点不同:预防是"静态约定、杜绝成因",避免是"动态判断、前瞻预防",检测是"事后发现",解除是"打破现状"。掌握"四条件"与"对应破坏策略"的映射是本题核心,例如"破坏循环等待=资源排序"是最常考查的一对。

#
★★★

2. 银行家算法的安全状态判定过程,请以具体资源分配矩阵演算一次?

请用具体资源分配矩阵演算银行家算法的安全状态判定过程?

  • 银行家算法基于最大需求声明,在分配前进行安全性检查。
  • 安全状态判定:存在一个安全序列,使系统可依次分配资源。
  • 用剩余资源(Available)逐步满足各进程的 Need 演算。

银行家算法要求每个进程声明其最大资源需求,系统在分配前判断"若分配后系统仍存在安全序列,则允许分配"。安全性检查算法:设 Available 为当前可用资源,Need[i] 为进程 i 还需的资源,Work=Available,反复寻找一个未完成且 Need[i] <= Work 的进程,假定其完成并将 Work += Allocation[i],直到所有进程都完成或找不到这样的进程。若存在一个使所有进程完成的顺序,则状态安全。例如系统有 3 类资源(A=10,B=5,C=7),进程 P0~P4 的 Allocation 与 Need 已知,初始 Available=(3,3,2),演算:先找 Need <= (3,3,2) 的进程(如 P1 Need=(1,2,2)),分配后 Work=(3,3,2)+(2,0,0)=(5,3,2),再找 Need <= (5,3,2) 的进程继续,若能完成全部进程则存在安全序列、状态安全。若某进程申请资源后残留 Available 不足以保证任何进程完成,则拒绝分配。

银行家算法是"避免死锁"的经典代表,其核心是"每次分配前做安全性检查"。演算要点是不断用"完成进程释放的资源"扩大 Work,直到覆盖所有进程。能手动演算一个安全序列是该算法面试题的标准求证方式。

#
★★★

3. 哲学家就餐问题的多种解法(资源排序/信号量限流/管程)对比?

哲学家就餐问题有哪些解法(资源排序、信号量限流、管程)?各有什么特点?

  • 资源排序(给筷子编号,按顺序取)可打破循环等待。
  • 信号量限流(最多允许 4 人同时拿筷子)避免死锁。
  • 管程/条件变量:将取筷与放下封装为原子操作,用条件变量等待。

哲学家就餐问题有经典解法。资源排序法:为每根筷子编号,要求每位哲学家先取编号较小的筷子再取编号较大的,从而打破循环等待,避免死锁。信号量限流法:引入一个计数信号量,限制最多允许 4 位哲学家同时尝试取筷子(即至少留 1 人),使不可能出现 5 人各自持一根筷子等待另一根的死锁局面。管程(Monitor)解法:把"取筷子"与"放下筷子"封装为管程内的原子操作,用条件变量管理状态,哲学家取不到筷子时在条件变量上等待,邻居放下筷子时 signal 唤醒,既保证互斥又避免忙等。此外还有 Chandy-Misra 分布式算法等。这些方法各从不同角度解决"死锁"与"饥饿",资源排序侧重打破循环等待,限流侧重保证不出现全部持有等待,管程侧重结构化封装与等待-通知。

哲学家就餐是死锁的经典模型,其解法本质是"破坏循环等待"或"保证至少一个能完成"。理解不同解法对应破坏的是哪个条件,是本题得分点;管程解法还体现了"条件变量 + 互斥"的现代同步思想。

#
★★★

4. 死锁的四个必要条件如何用"破坏其一"的思路设计解决方案?

如何用"破坏四个必要条件之一"的思路设计死锁解决方案?

  • 破坏互斥:使用可共享资源或假脱机。
  • 破坏占有且等待:一次性申请全部资源。
  • 破坏不可剥夺:允许资源被剥夺。

预防死锁的思路是破坏四个必要条件之一。破坏互斥:将独占资源改为可共享(如用假脱机打印、只读共享文件),但多数资源本质上不可共享,因此适用范围有限。破坏占有且等待:要求进程在开始前一次性申请其所需的全部资源,或运行时先释放已占资源再申请,代价是资源利用率低、可能长期闲置。破坏不可剥夺:允许系统在需要时剥夺某进程已占资源(如把资源分配给等待者),但需考虑资源本身状态能否安全保存恢复。破坏循环等待:为所有资源定义全局编号,要求每个进程按编号递增顺序申请资源,保证不出现等待环。这四种思路中,破坏循环等待(资源排序)最常用且实现代价低,是锁排序(lock ordering)的理论基础。

关键在于"四条件"与"破坏策略"的一一对应。破坏循环等待是工程上最实用的一招,因为它不需要改变资源本质、也不牺牲太多利用率,只需约定申请顺序。该题常与"锁排序预防死锁"结合考查。

#
★★★

5. 死锁检测的资源分配图,如何判断图中存在环,检测到死锁后的恢复策略(终止进程/资源剥夺)如何?

如何用资源分配图判断死锁?检测到死锁后的恢复策略有哪些?

  • 资源分配图用有向图表示进程与资源的关系,死锁判定依据是图中存在环。
  • 检测算法:对每个未被占用的资源进行资源分配,逐步归约,若能简化所有进程则无死锁。
  • 恢复策略:终止进程(一次终止一个/批量)与资源剥夺。

资源分配图(Resource Allocation Graph)用矩形表示资源、圆表示进程,请求边(进程→资源)与分配边(资源→进程)表达资源关系。若图中有环,则可能存在死锁;若每种资源类型只有一个实例,则环的存在等价于死锁;若资源类型可能有多个实例,则需用更复杂的安全检测(银行家算法的反向)。死锁检测算法:反复寻找可被满足的未独立进程(其请求的资源可由当前可用资源满足),令其完成并释放资源,重复归约;若最后所有进程都能被简化,则无死锁。检测到死锁后的恢复:一是终止进程——终止所有死锁进程或逐个终止并每次评估,直到打破死锁,需权衡终止数目与代价;二是资源剥夺——从部分进程剥夺资源分给其他进程,并回滚被剥夺进程到安全点,需维护可回滚状态。

检测与恢复是"事后处理"策略,与预防/避免不同,它允许死锁短暂发生再化解。判定"环"是核心,恢复的两种手段(终止进程、剥夺资源)都要权衡"代价最小化"与"系统一致性"。该题常与"何时启用检测、检测频率"结合让考生权衡。

#
★★★

6. 条件变量(Condition Variable)的 signal 与 broadcast 语义,为什么 broadcast 常被误用,条件变量与信号量在"丢失唤醒"(lost wakeup)问题上的本质差异如何?

条件变量的 signal 与 broadcast 语义有何不同?为什么 broadcast 常被误用?条件变量与信号量在"丢失唤醒"问题上有什么本质差异?

  • signal 只唤醒一个等待线程,broadcast 唤醒所有等待线程。
  • 当多个条件/多个线程共享一个条件变量时,用 signal 可能唤醒错误线程,故用 broadcast。
  • 条件变量无内部计数,wait 与 signal 必须配对且不可丢失;信号量有计数,能承载信号。

条件变量的 signal(notify)只唤醒一个等待线程,broadcast(notifyAll)唤醒所有等待线程。broadcast 常被误用是因为:当多个线程等待同一个条件变量但等待的是不同条件(谓词不同),或一个唤醒可能被某个线程消费后其他线程仍应检查时,用 signal 可能唤醒到一个条件仍不满足的线程,导致"唤醒失效";使用 broadcast 让所有线程都重新检查自己的谓词,逻辑更安全,代价是额外唤醒开销。条件变量与信号量的本质差异在于"丢失唤醒":条件变量没有内部计数,wait 释放锁并阻塞,signal 若在 wait 之前发出就会丢失(没有任何等待者,信号丢失),所以必须用"锁 + 循环检查谓词"保证不丢失;而信号量内部有计数器,signal(V 操作)会累积计数,之后 wait(P 操作)仍能取到,不会丢失。正因如此,条件变量强调"状态变化"、信号量强调"资源计数"。

正确使用条件变量必须遵守"在锁内检查谓词、用 while 循环而非 if 检查、wait 原子释放锁"。broadcast 相对 signal 更安全但会唤醒到无关线程,因此"唤醒哪些线程"取决于谓词语义。理解"条件变量无计数 vs 信号量有计数"是回答 lost wakeup 的关键。

#
★★

7. 生产者-消费者与读者-写者问题的信号量方案与写者饥饿问题?

用信号量如何解决生产者-消费者与读者-写者问题?写者饥饿问题是什么?

  • 生产者-消费者:用空槽计数、满槽计数与互斥锁三个信号量。
  • 读者-写者:用读写计数与互斥锁,读者优先时写者可能饥饿。
  • 写者饥饿:读者源源不断到来使写者永远得不到执行。

生产者-消费者问题用三个信号量:empty 表示空缓冲区数(初始为 N)、full 表示满缓冲区数(初始 0)、mutex 保护缓冲区互斥。生产者先 P(empty) 再 P(mutex) 放入再 V(mutex) V(full);消费者先 P(full) 再 P(mutex) 取出再 V(mutex) V(empty)。读者-写者问题:读者优先方案用 readcount 计数与 mutex 保护 readcount,写者用 wmutex 互斥;第一个读者进入时 P(wmutex),最后一个读者退出时 V(wmutex),写者每次 P(wmutex)。读者优先方案中,只要不断有读者到达,写者就可能长期无法进入,即写者饥饿。解决写者饥饿可引入写者优先或公平策略(如 FIFO 排队、读写锁的写者优先/公平模式),保证写者最终获得执行。

信号量方案的核心是"用计数信号量表达资源数量、用互斥信号量保护临界区"。读者-写者问题的关键是"第一个读者加锁、最后一个读者解锁"的计数技巧,以及由此引发的写者饥饿——这是引出公平性策略的入口。

#
★★

8. 活锁与饥饿和死锁的区别及各自的工程案例?

活锁、饥饿与死锁的区别是什么?各自有哪些工程案例?

  • 死锁:进程互相等待对方资源,都不推进。
  • 活锁:进程在持续改变状态但不真正推进,可能无限重试。
  • 饥饿:进程长期得不到所需资源而无法推进。

死锁是多个进程都在等待对方持有的资源,形成循环等待,都不推进(如 A 持锁 1 等锁 2、B 持锁 2 等锁 1)。活锁是进程没有死锁,但不断改变自身状态来试图避免冲突,却始终无法向前推进,类似"两人在窄巷互相让路却一直让来让去";工程上如无锁 CAS 重试在高冲突下可能表现为活锁,分布式锁冲突重试也可能活锁。饥饿是某个进程长期无法获得所需资源(如被高优先级进程不断抢占),但其他进程在正常推进;工程案例如读者优先的读写锁导致写者饥饿、CPU 优先级反转导致低优先级任务饥饿、公平性不足的调度器使某任务长期得不到执行。区别要点:死锁是"互相等、都不动",活锁是"都在动、却无进展",饥饿是"一个等、别人动"。

三者都表现为"进程无法完成",但机制不同:死锁是循环等待,活锁是忙而不进,饥饿是不公平。识别关键看"是否在推进/资源是否被抢"。该题常结合并发编程与调度场景让考生区分概念。

#
★★

9. 哲学家就餐问题的五种解法(信号量/资源分级/Chandy-Misra)各解决什么问题?

哲学家就餐问题的五种解法(信号量、资源分级、Chandy-Misra 等)各解决什么问题?

  • 信号量方案:用信号量保证互斥访问。
  • 资源分级(编号排序):打破循环等待,避免死锁。
  • Chandy-Misra:分布式算法,避免死锁与饥饿。

哲学家就餐问题的多种解法针对的问题不同。信号量方案为每根筷子设一个互斥信号量,保证同一时刻只有一人拿起某根筷子,解决"互斥访问";但若每位哲学家都先拿左筷再拿右筷,会形成循环等待导致死锁,于是需结合"资源分级"(给筷子编号,按编号顺序取)来打破循环等待,解决死锁。限流方案(限制最多 4 人同时就餐)保证至少一人能放下筷子,也避免死锁。Chandy-Misra 算法是分布式解法,通过"请求-许可-消息"机制动态协商,既避免死锁又避免饥饿(保证公平),适合无全局共享状态的环境。此外,把"取筷"与"放下"封装为原子操作(管程)可保证互斥并避免忙等。不同解法解决的核心问题可归纳为:互斥(信号量)、死锁(资源分级/限流)、饥饿与公平(Chandy-Misra、公平排队)。

哲学家就餐是并发模型的经典,解法众多,关键是识别"每种解法针对哪个问题"。资源分级针对死锁、Chandy-Misra 同时针对死锁与饥饿、管程针对结构化与互斥。掌握解法与问题对应关系即可。

#
★★

10. 生产者-消费者、读者-写者问题的信号量与管程实现差异?

生产者-消费者与读者-写者问题用信号量实现与管程实现有何差异?

  • 信号量:显式管理计数,需小心 P/V 顺序防止死锁。
  • 管程:把数据与操作封装,用条件变量,自动保证互斥,结构更清晰。
  • 语义差异:信号量是低层计数原语,管程是高层封装。

信号量实现中,生产者-消费者用 empty/full/mutex 三个信号量,读者-写者用 readcount 与互斥信号量,所有同步都靠显式的 P/V 操作,程序员需自行保证顺序正确、避免死锁,逻辑分散且易出错。管程实现则把共享缓冲区/读写状态及其操作封装成类,内部自动保证互斥(同一时刻只有一个线程进入管程),用条件变量表达"等待某个条件",当条件不满足时在条件变量上等待、条件变化时 notify——代码结构更清晰、正确性更好验证。差异总结:信号量是"面向资源计数"的通用原语,功能强大但需显式管理;管程是"面向对象"的封装,把互斥与等待的逻辑内聚,更易写对但依赖语言支持(Java synchronized/Wait/Notify、Go 的 cond)。因此现代语言多基于管程思想(monitor + condition variable)提供同步设施。

信号量与管程在表达能力上等价(都能实现这两类问题),但可维护性与出错概率不同。管程借助"互斥内建 + 条件变量"消除显式 P/V 出错,是面试常考"两者差异"的答案。记住"信号量靠计数、管程靠条件变量加自动互斥"。

#
★★

11. 工程上的锁排序(lock ordering),为什么所有线程按相同顺序加锁能预防死锁,违反时如何用工具发现?

为什么所有线程按相同顺序加锁能预防死锁?违反锁排序时如何用工具发现?

  • 按相同顺序加锁打破循环等待,避免死锁。
  • 违反锁排序在并发运行中可能形成锁环。
  • 工具:静态分析(如 ThreadSanitizer、lockdep)、运行期检测(jstack/线程转储)。

锁排序(lock ordering)要求所有线程以相同的全局顺序获取多把锁。若所有线程都先持锁 A 再持锁 B,则不可能出现"线程 X 持 A 等 B、线程 Y 持 B 等 A"的循环,从而打破循环等待条件,从源头上预防死锁。违反锁排序时,只有特定交错才可能形成锁环,因此难以靠测试发现。工程检测手段包括:Linux 内核的 lockdep 能自动检测锁序冲突并打印警告;静态/动态分析工具(如 ThreadSanitizer、Clang 的线程安全分析)检查锁序;运行时用 jstack 或线程转储抓取线程持有锁与等待锁的栈,若出现"互相等待"的环即可识别死锁。此外,编译器/运行时(Java 的 JFR、Go 的 copylock 检查)也能辅助发现。规范的团队可引入锁顺序文档与代码评审约束。

锁排序的本质是"用全局一致的申请顺序破坏循环等待"。它比死锁检测更主动,正确性可由约定保证。检测工具是基于"锁获取次序图"判断是否成环,理解这一点就能解释 jstack 如何识别死锁环。

#
★★

12. 可重入锁(recursive mutex)的适用与危害,为什么可重入锁会掩盖设计问题,信号量为何不可重入?

可重入锁的适用场景与危害是什么?为什么可重入锁会掩盖设计问题,信号量为何不可重入?

  • 可重入锁允许同一线程重复获取,用计数实现。
  • 适用:递归调用、回调中需要同一把锁。
  • 危害:掩盖"锁粒度过大/锁序混乱"的设计问题,便于误用。

可重入锁(recursive mutex)允许同一线程多次获取同一把锁而不死锁,内部用"持有者线程 + 计数"实现,每次 unlock 减计数,归零才真正释放。适用场景是递归函数或回调中会再次进入已持锁的临界区,且难以避免重复加锁。但其危害在于:它掩盖了锁粒度设计问题——正常设计应避免同一线程反复加锁,可重入锁让“在持锁状态下再调用会加锁的代码”这种不良结构也能运行,掩盖了锁序与粒度缺陷,同时增加计数维护开销、降低可读性,因此业界普遍建议慎用。信号量不具备所有权概念,没有记录“哪个线程持有”,因此不可重入:同一线程对同一信号量连续做 P 操作会因其计数归零而阻塞自身,造成死锁。可重入锁的本质是"锁有所有权",信号量是"计数无所有权"。

可重入锁与信号量的差异源于"是否有所有者"。可重入锁以"持有者线程"为前提,信号量只关心计数。理解"可重入锁掩盖设计问题"的观点,才能回答"为什么工程上慎用"这类规范类问题。

#
★★

13. 互斥锁的实现,TAS/CAS 与内核 futex 如何配合?

互斥锁如何在用户态与内核态实现?TAS/CAS 与 futex 各起什么作用?

  • 用户态自旋:TAS/CAS 原子操作尝试获取锁。
  • 内核态 + futex:竞争失败时睡眠,避免忙等。
  • 混合策略:先自旋再睡眠,兼顾低延迟与低 CPU 占用。

互斥锁的实现通常分用户态与内核态两层。用户态用原子操作尝试获取:TAS(test-and-set)把锁标志原子置 1 并返回旧值,若旧值为 0 说明获取成功;CAS(compare-and-swap)比较期望值并交换,若锁值等于期望值则置为已锁定。这些原子操作由硬件指令(x86 的 XCHG/LOCK CMPXCHG)保证。但仅靠自旋(spin)在多核争用下会浪费 CPU,因此现代互斥锁(如 Linux futex、pthread 的互斥锁)采用"先自旋、再睡眠":用户态先尝试 CAS 自旋片刻,若仍失败则调用 futex 系统调用,把当前线程放入等待队列并睡眠,让出 CPU;释放锁时唤醒等待者。futex 解决"用户态自旋与内核态睡眠"的衔接:快速路径(无竞争)完全在用户态完成,慢路径(有竞争)才进入内核,从而兼顾低延迟与低 CPU 占用。这就是高效互斥锁(如 Java 的锁优化、pthread mutex)的通用实现思路。

TAS/CAS 是"快速路径"的原子尝试,futex 是"慢路径"的睡眠等待。理解"先自旋后睡眠"的混合策略,能解释为何互斥锁在低竞争下开销小、高竞争下不忙等。这是并发底层实现的核心考点。

#
★★

14. 自旋锁与互斥锁的选择,临界区长短如何权衡?

自旋锁与互斥锁如何选择?临界区长短如何权衡?

  • 自旋锁:忙等,适合短临界区(微秒级),避免切换开销。
  • 互斥锁:阻塞睡眠,适合长临界区,避免浪费 CPU。
  • 权衡:切换成本 vs 自旋等待成本。

自旋锁在等待期间忙等(轮询测试锁),不释放 CPU,适合临界区很短(如仅更新一个计数器)、持锁时间远小于一次上下文切换开销的场景,因为忙等比切换更划算。互斥锁在获取失败时让线程睡眠、让出 CPU,避免忙等浪费,适合临界区较长(如 IO、复杂计算的临界区)或等待时间可能很长的场景。选择的核心权衡是"自旋等待的 CPU 成本"与"上下文切换 + 唤醒的成本":若临界区短,切换成本高,自旋更优;若临界区长,自旋会长时间占用 CPU,睡眠更优。现代内核与用户态锁常采用"自适应"或"先自旋后睡眠"的混合策略,兼顾两者。此外自旋锁在单核上无意义(忙等也会被抢占),且自旋锁持锁期间不可睡眠(如内核自旋锁持锁期间禁止睡眠与抢占)。

选择依据是"临界区长度与切换成本的相对大小"。把"临界区短选自旋、长选互斥"作为判断基准,再补充"自旋不可睡眠、单核无效"等边界,即可完整回答。工程上可用"自适应自旋"折中。

#
★★

15. 读写锁的公平性策略,读者优先/写者优先/公平锁对读写吞吐有何影响?

读写锁的读者优先、写者优先与公平策略对读写吞吐有何影响?

  • 读者优先:读吞吐高,但写者可能饥饿。
  • 写者优先:写者及时,但读者可能饥饿。
  • 公平锁:按到达顺序排队,读写公平,但吞吐略降。

读写锁的公平性策略影响读写吞吐与延迟。读者优先:允许读者源源不断进入,读吞吐最高,但写者可能因读者不断到来而长期饥饿,写延迟不可控。写者优先:一旦有写者等待,新到的读者需排队,写者能得到及时服务,避免写者饥饿,但可能让读者饥饿,且写者优先会降低读者并发度。公平锁(如 FIFO 排队):读写都按到达顺序排队,既不饥饿也不偏好,吞吐相对均衡但略低于极端策略,因为等待中的读者不会抢在写者前。此外还有"写者偏好的同时限制读者数量"等折中。实际选择取决于负载:读多写少且写延迟不敏感用读者优先;写延迟敏感(如日志、缓存失效)用写者优先;对公平性有要求用公平锁。Java 的 ReentrantReadWriteLock 默认非公平,可配置公平模式。

公平性本质是"在读写竞争点如何裁定"。三类策略在"读吞吐、写延迟、公平性"上各有取舍,读者优先牺牲写、写者优先牺牲读者、公平则折中。理解"饥饿"来源与策略取舍即可作答。

#
★★

16. 无锁同步与 ABA 问题,CAS 循环为何可能 ABA,版本号/标记如何解决,工程上如何权衡无锁与加锁?

CAS 循环为何可能遇到 ABA 问题?版本号/标记如何解决?工程上如何权衡无锁与加锁?

  • CAS 只比较值,无法区分"值从 A 变 B 再变回 A"的过程,即 ABA。
  • 解决:用版本号/标记(如 AtomicStampedReference)、或带 ABA 问题的数据结构可采用指针标签。
  • 权衡:无锁并发高、无阻塞,但正确性难、ABA 处理复杂;加锁简单可靠,但有争用开销。

CAS 比较的是"期望值"与"当前值"是否相等,若相等则交换。当某个值从 A 变为 B 再变回 A,CAS 会误判"未发生变化"而成功,这就是 ABA 问题。例如在无锁栈中,线程 A 观察栈顶为节点 X,期间线程 B 弹出 X 又压入新节点 X(地址可能复用),A 的 CAS 仍成功,但栈结构已被破坏。解决方案是引入版本号/标记:每次修改都递增版本号,CAS 同时比较"值+版本"(如 Java 的 AtomicStampedReference、AtomicMarkableReference),从而识别"值虽回 A 但版本已变"。工程上权衡:无锁编程(lock-free)在低竞争下吞吐高、无死锁/无阻塞,但正确性验证难、需处理 ABA 与内存序,代码复杂度高;加锁简单、可读性强、易正确,但高争用下会因睡眠/唤醒与锁迁移损失吞吐。因此"无锁 vs 加锁"需结合争用程度、正确性要求、性能目标权衡,通常先在加锁版本上验证正确性,再对热点做无锁优化。

ABA 是 CAS 的经典陷阱,核心是"CAS 只认值、不认过程"。版本号解决"值相等但过程变化"的识别。工程权衡的主线是"正确性 vs 性能",无锁在高并发低争用下才有优势,否则加锁更划算。

#
★★

17. 屏障(Barrier)同步,多线程阶段化并行中的 barrier 语义,与条件变量的实现差异如何?

屏障(Barrier)同步的语义是什么?如何在多线程阶段化并行中使用?与条件变量实现有何差异?

  • Barrier 语义:所有线程到达屏障后一起等待,直到全部到达才同时放行。
  • 用于阶段化并行:每一阶段结束处同步,保证数据就绪。
  • 与条件变量差异:barrier 是"全体到达放行",条件变量是"条件变化通知",实现需计数。

屏障(barrier)是一种同步原语,其语义是"所有参与的线程必须都到达屏障点,才能继续执行"。当某线程到达屏障时,它会阻塞等待,直到参与线程全部到达,屏障才"打开",所有线程同时放行进入下一阶段。它常用于阶段化并行(如分块矩阵运算、迭代计算、数值模拟的每一轮同步):每轮各线程并行计算,结束处用 barrier 等待所有线程完成,避免某线程读到未更新的数据。实现上通常用一个计数器统计到达线程数,并配合互斥锁与条件变量:到达线程加锁递增计数,若未到齐则条件变量等待,当最后一个到达时广播唤醒所有等待线程并重置计数。与条件变量的差异:条件变量是"等待某个条件成立的事件通知",语义是"等待者被唤醒后还需自行检查条件";barrier 是"等待全体到达"的集合同步,语义更明确、需要计数与"放行-重置"两阶段,且通常可复用(生成循环屏障)。C++ 的 std::barrier、Java 的 CyclicBarrier 都是其实现。

barrier 的关键是"全体到达才放行 + 可复用",与条件变量"条件变化通知"不同。理解其"计数 + 互斥 + 条件变量 + 重置"的实现,就能解释为何 barrier 是条件变量之上的应用。阶段化并行是其主要应用场景。

#
★★

18. 死锁的工程检测,jstack/线程转储中如何识别死锁环,生产环境发现死锁后的处置流程如何?

如何用 jstack/线程转储识别死锁环?生产环境发现死锁后应如何处置?

  • jstack 能输出线程持有锁与等待锁的栈,识别 A 等 B、B 等 A 的环。
  • 识别方法:找线程 "waiting to lock" 与 "locked" 的对应关系。
  • 处置流程:先确认,再解除(kill 触发线程/重启),再根治(锁序、锁粒度)。

jstack 生成线程转储(thread dump),每个线程块的栈里会显示线程持有的锁(locked)与等待的锁(waiting to lock)、以及锁的持有者("held by thread X")。识别死锁环的方法是:寻找线程 A 等待的锁被线程 B 持有,同时线程 B 等待的锁被线程 A 持有,形成 A↔B 的等待环;Java 的 jstack 甚至会在末尾明确打印 "Found one Java-level deadlock" 并列出环。生产环境处置流程:先快速确认(多次转储对比,确认锁环稳定存在),评估影响范围;紧急解除可 kill 掉环中代价最小的线程或重启相关服务;随后根治——修复锁获取顺序(统一 lock ordering)、降低锁粒度、避免持锁调用外部/重操作、必要时用超时锁(tryLock)避免无限等待;最后通过监控与压测验证。对非 JVM 环境,可用 gdb、pstack、Linux 的 /proc//stack 或内核 lockdep 检测。

识别死锁环的核心是"构造锁与线程的等待图,找环":jstack 恰好给出"持有/等待"信息。处置遵循"确认→解除→根治→验证"四步,根治手段与锁排序、锁粒度等预防原则一致。该题常与运维场景结合。

#

19. 自旋锁、互斥锁、读写锁在用户态/内核态的实现开销对比?

自旋锁、互斥锁、读写锁在用户态与内核态的实现开销有何对比?

  • 自旋锁:用户态忙等,无上下文切换,但争用下耗时。
  • 互斥锁:竞争时进入内核睡眠/唤醒,有切换开销。
  • 读写锁:逻辑更复杂,读路径开销小,写路径需独占。

自旋锁在用户态或内核态通过原子操作忙等,无上下文切换,无竞争时开销最低(仅一次原子操作),但争用激烈时 CPU 被空转占用,总体开销随争用上升。互斥锁无竞争时开销也低(用户态 CAS 快速路径),但竞争失败时需进入内核(futex)让线程睡眠并在释放时唤醒,产生系统调用与上下文切换开销,比自旋重。读写锁在无竞争时读路径开销略高于互斥锁(需维护读写计数与状态),但读读并发时能提升吞吐;写路径需独占,开销与互斥锁相当,且公平性策略(如等待队列)会增加复杂度。综合开销对比:无竞争时自旋≈互斥(快速路径)<读写锁;高竞争时自旋开销最大(CPU 空转),互斥与读写锁因睡眠/唤醒承担切换开销但能释放 CPU。选择取决于临界区长度与争用程度。

开销对比的核心是"快速路径(无竞争)"与"慢路径(争用)"的差异,以及"忙等 vs 睡眠"的取舍。自旋省切换但耗 CPU,互斥/读写省 CPU 但耗切换。读写锁额外收益是"读读并行",故读多写少时即使实现稍复杂也划算。

#

20. 读写锁与哲学家就餐,避免死锁的解法如何?

读写锁与哲学家就餐问题如何结合,避免死锁的解法是什么?

  • 哲学家就餐的两根筷子可视为共享资源,避免死锁需打破循环等待。
  • 读写锁强调读读并行、写写互斥,与就餐问题的"资源互斥"对比。
  • 结合点:用一致性锁序/资源编号避免死锁。

哲学家就餐问题中每位哲学家需同时持有两根筷子,若随意取用会形成循环等待而死锁。读写锁的背景是"共享资源允许并发读、互斥写",其读读并行、写写互斥的思想与"筷子互斥"有本质区别——就餐问题中筷子是独占资源,不存在共享读。将两者结合避免死锁的通用解法是:为所有资源(筷子)建立全局一致编号,让每位哲学家按编号升序取筷,从而打破循环等待,避免死锁;或在读写锁的使用中也遵循统一锁序(如先锁资源 A 再锁资源 B),防止多把锁交叉等待成环。本质上,无论读写锁还是就餐问题,避免死锁的关键都是"一致的资源申请顺序"或"保证至少一个能完成"。若把两根筷子视为"读锁"与"写锁",则需注意锁顺序的一致性与生命周期管理,避免持有一把锁等另一把锁。

该题把两个经典话题缝合:就餐问题"资源独占、需成对持有"与读写锁"读共享写独占"对比,共同解法落点是"锁序一致打破循环等待"。回答时强调"资源编号 + 统一顺序"即可避免死锁。