程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

Java面试必备:死锁、活锁、饥饿的核心区别

balukai 2025-07-10 13:08:44 文章精选 2 ℃

这是一个经典的并发编程面试题。理解死锁、活锁和饥饿的区别对于设计和诊断健壮的并发系统至关重要。下面详细解释它们的区别:

核心概念区别

特征

死锁 (Deadlock)

活锁 (Livelock)

饥饿 (Starvation)

定义

两个或多个线程/进程永久阻塞,每个都持有对方需要的资源并等待对方释放自己需要的资源。

线程/进程持续运行(不阻塞),但不断改变状态以避免死锁或冲突,导致无法取得实质性进展

一个或多个线程/进程长期或永久无法获取所需资源(通常是CPU时间片或锁),导致无法执行或进展极其缓慢

状态

阻塞状态 (Blocked/Waiting)。线程停止执行,等待永远不会到来的资源。

运行状态 (Runnable/Running)。线程仍在消耗CPU,但没有实际工作完成。

可运行状态 (Runnable),但很少或从未被调度执行;或者在等待资源,但资源总是被优先分配给其他线程。

资源持有

持有至少一个资源,并等待获取另一个被其他线程持有的资源。

不持有任何阻塞性资源(或短暂持有后立即释放),或者主动让出资源以避免冲突。

不持有所需的资源,并尝试获取但总是失败。

进展

完全无进展。系统完全卡住。

表面上有进展(线程在运行),但实质无进展(任务未完成)。

部分线程无进展或进展极慢,而其他线程正常推进。

原因

循环等待 + 互斥 + 不可抢占 + 持有并等待(四个必要条件同时满足)。

过于“礼貌”或“退让”的冲突解决策略,导致双方不断重复相同的退让动作。

资源分配策略不公平(如低优先级线程、锁竞争策略不合理、资源不足)。

解决方案

破坏必要条件之一(如引入超时、按顺序获取锁、死锁检测与恢复)。

引入随机性退让策略(如随机等待时间、退让次数限制)。

实现公平性机制(公平锁、优先级调整、轮转调度、确保资源充足)。

是否可恢复

通常需要外部干预(超时机制、人工重启)才能恢复。

可能自行恢复(如果随机性或策略导致一方成功),但概率较低,通常也需要干预。

可能自行恢复(如果高优先级线程完成),但也可能永久持续,需调整策略。

深入解析与对比

1. 死锁 (Deadlock) vs. 活锁 (Livelock)

  • 核心区别:状态与进展
    • 死锁: 线程被阻塞不做任何事,完全僵持。就像两辆车在一条窄路上迎面相遇,谁也不肯倒车让路,都停在那里不动了。
    • 活锁: 线程没有阻塞在“忙”,但这种“忙”是在做无效的协调工作(反复尝试、失败、重试相同的策略)。就像走廊里两个人迎面走来,都想给对方让路,结果你向左我也向左,你向右我也向右,不断重复避让却始终面对面挡着路,无法通过。
  • 比喻:
    • 死锁: 两个骑士在独木桥上相遇,互不相让,都举着盾牌和剑僵持着,谁也无法前进或后退(永久阻塞)。
    • 活锁: 两个骑士在独木桥上相遇,都想让对方先过。骑士A说“您先请”并后退一步,同时骑士B也说“您先请”也后退一步,结果两人同时后退又同时回到桥中间。他们不断重复这个“礼貌退让”的动作,虽然都在动,但谁也没过桥(无实质进展)。

2. 死锁 (Deadlock) vs. 饥饿 (Starvation)

  • 核心区别:资源持有与原因
    • 死锁: 涉及多个线程,每个线程都持有部分资源,并等待其他线程持有的资源。是相互阻塞的循环。所有相关线程都停滞。
    • 饥饿: 通常涉及一个或多个线程,这些线程无法获得所需资源(通常是CPU时间或锁),而其他线程(通常优先级更高或更“幸运”)能持续获得资源并执行。饥饿的线程没有持有阻塞性资源,只是在等待。系统整体可能仍在推进(因为有线程在执行),但部分线程被“饿死”。
  • 比喻:
    • 死锁: 四个哲学家围坐一桌,每人左边有一把叉子。要吃饭需要同时拿到左边和右边的叉子。如果每个人都拿起自己左边的叉子,然后等待右边的叉子,那么所有人都会永远等待下去(持有资源并等待他人释放)。
    • 饥饿: 一个水龙头前有很多人排队打水。总有几个强壮的人(高优先级)每次都能挤到前面打到水。而一个瘦弱的人(低优先级)每次都被挤到最后面,永远轮不到他打水(无法获得资源)。

3. 活锁 (Livelock) vs. 饥饿 (Starvation)

  • 核心区别:线程状态与原因
    • 活锁: 线程在运行(消耗CPU),但行为是无效的循环尝试。通常由主动的冲突解决策略导致(过度退让)。相关线程都在“忙活”但没结果。
    • 饥饿: 线程大部分时间处于可运行状态(等待调度)或等待资源状态,但很少或从未被调度执行或获得资源。由被动的、不公平的资源分配策略导致(调度算法、锁策略)。饥饿线程通常很“安静”(没机会运行)。

Java 代码示例 (简化版)

死锁示例

java

public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread1: Holding lockA...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread1: Waiting for lockB...");
                synchronized (lockB) {
                    System.out.println("Thread1: Acquired lockB!");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lockB) { // 与 thread1 获取锁的顺序相反
                System.out.println("Thread2: Holding lockB...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread2: Waiting for lockA...");
                synchronized (lockA) {
                    System.out.println("Thread2: Acquired lockA!");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}
  • 结果: Thread1 持有 lockA 等待 lockB,Thread2 持有 lockB 等待 lockA。两者永久阻塞。输出会卡在 Thread1: Waiting for lockB... 和 Thread2: Waiting for lockA...。

活锁示例 (过度退让)

java

public class LivelockDemo {
    static class Spoon {
        private Diner owner;
        public Spoon(Diner d) { owner = d; }
        public synchronized void use() { System.out.println(owner.name + " is eating!"); }
    }

    static class Diner {
        private String name;
        private boolean isHungry;
        public Diner(String n) { name = n; isHungry = true; }
        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) {
                // 如果勺子不是自己的,就等一下再试 (过度礼貌)
                if (spoon.owner != this) {
                    try { Thread.sleep(100); } catch (InterruptedException e) { continue; }
                    continue;
                }
                // 如果配偶也饿,先让给配偶 (过度退让)
                if (spouse.isHungry) {
                    System.out.println(name + ": You eat first, dear " + spouse.name);
                    spoon.owner = spouse; // 把勺子让出去
                    continue;
                }
                // 自己吃
                spoon.use();
                isHungry = false;
                System.out.println(name + ": I'm done eating!");
                spoon.owner = spouse; // 吃完后把勺子给配偶
            }
        }
    }

    public static void main(String[] args) {
        final Diner husband = new Diner("Bob");
        final Diner wife = new Diner("Alice");
        final Spoon spoon = new Spoon(husband); // 勺子初始属于丈夫

        Thread t1 = new Thread(() -> husband.eatWith(spoon, wife));
        Thread t2 = new Thread(() -> wife.eatWith(spoon, husband));

        t1.start();
        t2.start();
    }
}
  • 结果: Bob 和 Alice 都非常礼貌且饥饿。Bob 看到勺子是他的,但发现 Alice 也饿,就说“Alice 你先吃”并把勺子让给 Alice。同时 Alice 看到勺子现在是她的了,但发现 Bob 也饿,就说“Bob 你先吃”并把勺子让回给 Bob。这个过程无限循环,两人都在运行 (while (isHungry)),都在让勺子 (spoon.owner = spouse),但谁也没吃到 (spoon.use() 从未执行)。控制台会不断输出 Bob: You eat first, dear Alice 和 Alice: You eat first, dear Bob。

饥饿示例 (不公平锁竞争)

java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class StarvationDemo {
    private static final Lock lock = new ReentrantLock(); // 默认非公平锁,可能加剧饥饿

    public static void main(String[] args) {
        // 高优先级线程 (更容易抢到锁)
        Thread highPriorityThread = new Thread(new Worker("High-Prio"));
        highPriorityThread.setPriority(Thread.MAX_PRIORITY);
        highPriorityThread.start();

        // 多个低优先级线程 (很难抢到锁)
        for (int i = 0; i < 5; i++) {
            Thread lowPriorityThread = new Thread(new Worker("Low-Prio-" + i));
            lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
            lowPriorityThread.start();
        }
    }

    static class Worker implements Runnable {
        private String name;
        public Worker(String name) { this.name = name; }

        @Override
        public void run() {
            while (true) {
                lock.lock(); // 尝试获取锁
                try {
                    System.out.println(name + " acquired the lock and is working...");
                    // 模拟工作耗时
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                } finally {
                    lock.unlock();
                }
                // 短暂休眠,模拟其他工作
                try { Thread.sleep(10); } catch (InterruptedException e) {}
            }
        }
    }
}
  • 结果 (可能): High-Prio 线程由于其高优先级和 ReentrantLock 默认的非公平特性,更频繁、更容易地获取到锁并执行工作。而 Low-Prio-0 到 Low-Prio-4 这些低优先级线程,很难甚至几乎无法获取到锁(输出中很少或几乎看不到它们获取锁的消息)。它们在 lock.lock() 处等待,或者在 Runnable 状态等待调度器分配CPU时间片(但调度器倾向于选择高优先级线程),从而长期无法执行实际工作,处于饥饿状态。

总结要点 (面试回答)

  1. 死锁 vs 活锁:
  2. 死锁: 线程阻塞不动持有资源等待另一个资源,无任何进展。是僵局
  3. 活锁: 线程持续运行不持有关键资源(或主动释放),不断尝试避让冲突,但无实质进展。是无效忙碌
  4. 死锁 vs 饥饿:
  5. 死锁: 多个线程相互阻塞,都持有部分资源并等待对方资源,所有相关线程都停滞
  6. 饥饿: 一个或多个线程长期无法获取资源(CPU时间或锁),其他线程能正常执行。饥饿线程在等待状态未持有阻塞性资源
  7. 活锁 vs 饥饿:
  8. 活锁: 线程在运行,因过度协调/避让策略导致无效循环。
  9. 饥饿: 线程很少或从未被调度运行,或无法获得锁,因资源分配不公导致。

关键记忆点:

  • 死锁 = 堵死不动 (Blocked, Hold & Wait)
  • 活锁 = 瞎忙活 (Running, No Progress)
  • 饥饿 = 轮不到 (Runnable but Never Scheduled, Can't Acquire Resource)

理解这些区别及其成因,有助于在设计和调试并发程序时识别问题并选择合适的解决方案(如锁顺序、超时、随机退避、公平锁、优先级调整等)。

最近发表
标签列表