真实面经题目 · 原创解析
死锁产生的条件和排查方式是什么?
死锁本质上是多个执行单元在持有部分资源的同时继续等待对方释放资源,导致所有相关线程都无法向前推进。面试回答要先讲清四个必要条件,再说明排查时如何从现象、线程状态、锁拥有关系、业务调用链和复现路径逐层定位,最后补充预防策略,例如统一加锁顺序、缩短锁范围、使用超时锁和减少共享可变状态。
真实面经题目 · 原创解析
死锁本质上是多个执行单元在持有部分资源的同时继续等待对方释放资源,导致所有相关线程都无法向前推进。面试回答要先讲清四个必要条件,再说明排查时如何从现象、线程状态、锁拥有关系、业务调用链和复现路径逐层定位,最后补充预防策略,例如统一加锁顺序、缩短锁范围、使用超时锁和减少共享可变状态。
死锁产生需要同时满足四个必要条件:第一是互斥,某个资源同一时刻只能被一个线程占用;第二是占有并等待,线程已经拿到一部分资源,又继续等待其他资源;第三是不可抢占,资源不能被外部强制剥夺,只能由持有者主动释放;第四是循环等待,多个线程之间形成了环形依赖,比如线程 A 持有锁 1 等锁 2,线程 B 持有锁 2 等锁 1。排查时我会先看现象,比如接口一直不返回、线程数稳定但吞吐下降、CPU 不一定很高。然后抓线程转储,重点看 BLOCKED、WAITING、parking 状态,以及线程正在等待哪个锁、该锁被哪个线程持有。对于 Java 程序,可以用线程转储、运行时诊断工具或 ThreadMXBean 查找 Java 层死锁。定位后再回到代码,确认加锁顺序、锁嵌套、异常释放、回调中反向加锁等路径。解决上优先破坏循环等待或占有并等待,比如统一锁顺序、减少锁嵌套、用 tryLock 设置超时、把共享状态收敛到更小范围,并保证 finally 中释放资源。
死锁不是单纯的慢,也不是某个线程执行时间长,而是多个线程之间形成了资源等待闭环,使每个参与者都在等待别人先释放资源。只要这个等待关系不被外部打断,相关线程就无法继续执行。它常出现在多个锁、多个连接、多个队列资源或锁与外部资源混用的场景中。判断时要看是否存在互相持有和互相等待,而不是只看线程状态是否为阻塞。
经典必要条件是互斥、占有并等待、不可抢占、循环等待。互斥说明资源不能同时共享;占有并等待说明线程拿着已有资源不放,又申请新资源;不可抢占说明其他线程不能强行拿走资源;循环等待说明等待关系形成闭环。它们是必要条件,不一定是充分排查步骤。工程上通常从循环等待和占有并等待入手,因为这两点最容易通过设计约束破坏。
最典型的场景是两个线程以不同顺序获取两把锁,例如一个流程先锁用户再锁订单,另一个流程先锁订单再锁用户。更隐蔽的场景包括同步方法内部调用外部回调、持锁期间访问数据库或远程服务、多个锁和线程池任务互相等待、读写锁升级、以及锁和连接池资源交叉等待。真实系统中死锁往往不是一眼可见的两把锁,而是跨模块调用形成的等待链。
排查先从运行现象入手:请求挂起、吞吐突然下降、部分功能卡死、线程池活跃线程耗尽、日志停在某个固定步骤。随后抓取线程转储,重复抓几次比只抓一次更可靠,因为可以确认线程是否长期停在同一锁等待点。重点关注线程名、调用栈、等待的监视器或同步器、锁的拥有者,以及业务日志中最后一次成功进入临界区的位置。
Java 中排查通常依赖线程转储和运行时诊断。线程转储可以展示线程当前栈、锁等待关系、已经持有的监视器,以及可重入锁等同步器信息。程序内也可以通过 ThreadMXBean 查找已知类型的死锁。需要注意的是,诊断工具能很好发现 Java 层锁死锁,但如果等待发生在数据库锁、文件锁、网络调用、外部进程或本地代码中,线程转储只能提供调用栈线索,仍要结合外部资源的等待信息分析。
拿到等待链后,要把线程栈映射回业务流程,找出每个线程先持有哪些资源、后等待哪些资源,再确认这条路径是否稳定可复现。排查时不能只改当前报错行,因为死锁通常来自全局加锁规则不一致。应检查所有获取同一组锁的入口,尤其是工具类、监听器、回调、事务边界和异常路径,确认是否存在反向加锁、持锁调用外部方法、释放逻辑缺失等问题。
解决死锁的核心是破坏必要条件。最常用的是统一锁顺序,让所有路径都按同一资源排序获取锁,从而破坏循环等待。也可以减少锁粒度和锁嵌套,避免持有一个锁时等待另一个慢资源。对可重入锁可使用 tryLock 加超时,失败后释放已持有资源并重试。更长期的方案是降低共享可变状态,使用不可变对象、消息队列、单线程串行化或更清晰的资源所有权模型。
死锁是多个线程之间形成循环等待,参与线程都无法继续推进;线程饥饿是某些线程长期拿不到运行机会或资源,但系统整体可能仍在运行。饥饿通常和调度策略、锁不公平、优先级设置有关,不一定存在等待闭环。
循环等待的前提是不同线程按不同顺序持有资源并等待后续资源。如果所有线程都按同一规则获取锁,例如先小编号后大编号,就不会出现一个线程等后面的资源、另一个线程反向等待前面资源的闭环。
tryLock 可以设置获取锁失败后的退出或重试策略。线程拿不到后续锁时,可以释放已经持有的锁,稍后重新竞争,从而破坏占有并等待条件。不过它需要设计好重试次数、退避策略和失败处理,否则可能引入活锁或业务失败。
不一定。BLOCKED 只说明线程正在等待进入某个同步区域,可能只是正常竞争。判断死锁要看等待是否长期存在,以及锁拥有者是否又在等待当前线程或等待链中的其他资源。多次采样和锁拥有关系比单个状态更可靠。
首先保留现场证据,连续抓取线程转储、保存关键日志和监控指标,避免直接重启导致线索丢失。如果业务已经不可用,可以在取证后做恢复操作。之后根据等待链定位代码路径,修复加锁顺序或持锁范围问题,并补充复现测试。