编写一段代码,使得这段代码必定会产生死锁,不能使用 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) 破「不可剥夺」,拿不到就释放已有锁重试,这题答完一般面试官会接着问这两个点。