Skip to content

编写一段代码,使得这段代码必定会产生死锁,不能使用 Thread.sleep() ​

  • 标签:后端、场景题、 Java

🏛 业务架构师 ​

经典写法是两个线程以相反顺序竞争两把锁,并用 CountDownLatch 让双方先同时拿到第一把锁,死锁必然发生:

java
Object lockA = new Object();
Object lockB = new Object();
CountDownLatch latch = new CountDownLatch(2);

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        latch.countDown();          // 报告已持有 A
        try { latch.await(); }      // 等对方也持有 B
        catch (InterruptedException e) {}
        synchronized (lockB) {}     // 持A等B -> 死锁
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        latch.countDown();
        try { latch.await(); }
        catch (InterruptedException e) {}
        synchronized (lockA) {}     // 持B等A -> 死锁
    }
});
t1.start(); t2.start();

关键点:用 latch 栅栏替代 sleep 保证时序确定性——两个线程各自持有一把锁、再互相等待对方持有的锁,形成循环等待,这是死锁四条件(互斥、持有并等待、不可剥夺、循环等待)同时满足的最小构造。

架构师视角我会补两句业务提醒:

  • 死锁在生产上比想象中常见:转账场景锁两个账户、库存和优惠券两把锁按不同顺序加,低并发时测试根本触发不了,高峰期线程互相卡死,接口大面积超时——本质是资损前兆。
  • 预防手段是工程规约:全局定义锁的获取顺序(如按资源 ID 排序)、用 tryLock(timeout) 主动放弃打破"不可剥夺"条件、上线前用工具(如 jenkins 集成的并发测试)验证。面试写出代码只是及格,讲出怎么在业务代码里防住才是加分项。

🏗 系统架构师 ​

死锁的充分条件是持有并等待 + 循环等待,所以构造两个线程以相反顺序抢两把锁即可。为了"必定"发生且不依赖 sleep,我用 CountDownLatch 强制同步双方时序:各自拿到第一把锁后,都等对方发信号再抢第二把锁。

java
public class MustDeadLock {
    static final Object a = new Object();
    static final Object b = new Object();
    static final CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a) {
                latch.countDown();          // 报告:我已持有 a
                try { latch.await(); } catch (InterruptedException e) {}
                // 确保对方也已持有 b,随后必然互相等待
                synchronized (b) {}
            }
        }).start();

        new Thread(() -> {
            synchronized (b) {
                latch.countDown();
                try { latch.await(); } catch (InterruptedException e) {}
                synchronized (a) {}
            }
        }).start();
    }
}

原理:latch 保证两个线程都持有第一把锁、并停在 await 处;双方放开后同时去申请对方持有的锁,不存在任何调度侥幸,必然死锁。

架构视角的延伸(面试加分点):真实系统里死锁更多发生在分布式锁上——两个服务以不同顺序拿多个资源的锁。防御手段是全局统一的加锁顺序(按资源 ID 排序)、tryLock 带超时放弃持有、以及数据库层面 innodb_lock_wait_timeout 兜底。JVM 层可以用 jstack 直接看到 "Found one Java-level deadlock"。

💻 全栈开发专家 ​

经典做法:两个线程各自持有一把锁、再去申请对方持有的锁,用 CountDownLatch 控制先后顺序代替 sleep,死锁必然发生。

java
public class MustDeadLock {
    static final Object A = new Object();
    static final Object B = new Object();
    static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (A) {
                System.out.println("T1 拿到 A,等 T2 就绪");
                try { latch.await(); } catch (InterruptedException e) {}
                System.out.println("T1 尝试拿 B ...");   // 此时 B 已被 T2 持有
                synchronized (B) { }
            }
        }, "T1").start();

        new Thread(() -> {
            synchronized (B) {
                latch.countDown();                      // T1 开始申请 B
                System.out.println("T2 拿到 B,尝试拿 A ...");
                synchronized (A) { }                    // A 被 T1 持有 → 互等
            }
        }, "T2").start();
    }
}

为什么必定死锁:T1 持有 A 等 B,T2 持有 B 等 A,且两个线程都不释放已持有的锁(synchronized 块没结束),形成循环等待,四个死锁必要条件(互斥、持有并等待、不可剥夺、循环等待)全部满足。

验证与排查:程序挂住后 jstack <pid>,最后一段会直接打印 Found one Java-level deadlock 并指出两个线程互相等哪把锁。

顺带说防治:实际项目里我用统一加锁顺序(比如按账户 ID 排序后再加锁)破环循环等待;或者用 tryLock(timeout) 破「不可剥夺」,拿不到就释放已有锁重试,这题答完一般面试官会接着问这两个点。