死锁与活锁排查:死锁产生的四个必要条件、死锁检测与恢复、活锁与饥饿现象分析

说到并发编程里的死锁,我脑子里立刻浮现出几年前的一个深夜。

那会儿线上一个支付服务突然卡死,所有线程都挂住了。我登录上去一看,四个线程互相等着对方释放锁,谁也不让谁。说白了,就是典型的死锁。那次事故让我彻底明白了——死锁不是理论问题,是真能让你半夜爬起来修Bug的。

死锁产生的四个必要条件

死锁要发生,必须同时满足四个条件。缺一个都不行。我个人习惯把这四个条件记成「互请不环」,好记又形象。

条件 说明 我的理解
互斥条件 资源一次只能被一个线程占用 说白了就是锁只能一个人拿
请求与保持 线程拿着一个锁,还去请求另一个锁 手里有碗,还想要锅
不剥夺条件 资源不能被强制拿走 除非你主动释放,别人抢不走
循环等待 形成一条等待环路 你等我,我等他,他等你

我在项目中遇到过最典型的场景:线程A持有锁1,请求锁2;线程B持有锁2,请求锁1。嗯,这就是教科书级别的死锁。

核心要点:四个条件必须同时成立才会死锁。只要破坏其中一个,死锁就解了。

死锁检测与恢复

死锁检测这事儿,我建议分两步走:运行时检测和事后分析。

运行时检测

Java里可以用 ThreadMXBean 来检测死锁。我一般这么写:

ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
if (deadlockedThreads != null) {
    // 有死锁,打印线程信息
    ThreadInfo[] infos = bean.getThreadInfo(deadlockedThreads);
    for (ThreadInfo info : infos) {
        System.out.println("死锁线程: " + info.getThreadName());
    }
}

这段代码我放在监控脚本里,每5秒跑一次。一旦检测到死锁,就触发告警。

恢复策略

检测到死锁后怎么恢复?我常用的方法有三种:

  • 强制终止线程:杀掉其中一个线程,释放它的锁。代价是可能丢失数据。
  • 回滚事务:让线程回滚到安全点,释放所有资源。适合数据库场景。
  • 资源抢占:强制剥夺某个线程的资源。嗯,这个实现起来比较麻烦。

注意:我曾经在线上用过强制终止线程的方式,结果导致一笔订单状态不一致。后来我改成了先记录日志,再人工介入处理。自动化恢复要谨慎。

活锁与饥饿现象分析

活锁和饥饿,这两个问题比死锁更隐蔽。你想想看,线程明明在运行,但就是干不了正事。

活锁

活锁是什么?两个线程互相谦让,结果谁也没法干活。

举个例子:线程A和线程B都需要锁1和锁2。A拿到锁1,发现锁2被B占着,就释放锁1说「你先来」。B拿到锁2,发现锁1被A占着,也释放锁2说「你先来」。结果两个线程一直在让来让去,就是干不了活。

我在项目中遇到过类似情况。一个消息队列的消费者,两个线程互相重试,导致消息处理延迟飙升。后来我加了个随机等待时间,打破了这种「礼貌性死循环」。

避坑指南:我曾经在重试逻辑里忘了加随机退避,结果活锁把CPU吃满了。记住,重试一定要加随机延迟,别用固定间隔。

饥饿

饥饿比活锁更惨。一个线程一直拿不到资源,永远等下去。

常见原因有两个:

  • 优先级倒置:低优先级线程持有锁,高优先级线程等着。高优先级线程一直抢不到CPU。
  • 不公平锁:某些线程总是被跳过,永远拿不到锁。

我建议用公平锁来解决饥饿问题。Java里的 ReentrantLock(true) 就是公平锁,它会按请求顺序分配锁。

// 公平锁示例
ReentrantLock fairLock = new ReentrantLock(true);

public void doWork() {
    fairLock.lock();
    try {
        // 业务逻辑
    } finally {
        fairLock.unlock();
    }
}

不过要注意,公平锁性能比非公平锁差一些。我一般只在确实有饥饿风险时才用。

总结一下:死锁是线程互相等待,活锁是线程互相谦让,饥饿是线程被冷落。三种问题都要重视,但处理方式不一样。

并发问题排查知识体系 死锁 四个必要条件 检测与恢复 活锁 互相谦让导致无法推进 饥饿 线程被冷落无法获取资源 解决方案:破坏必要条件 | 随机退避 | 公平锁 | 超时机制 排查思路:先判断是哪种问题,再选择对应的恢复策略 💡 我的经验:先看线程堆栈,再分析锁的持有关系

排查并发问题,我个人的习惯是:先看线程堆栈,找出所有等待的线程。然后分析锁的持有关系,判断是死锁、活锁还是饥饿。最后对症下药。

嗯,记住一点:预防永远比恢复更重要。写代码时多想想锁的顺序,多用超时机制,能省去很多半夜排查的麻烦。

可可精品资料库