饿了么Java面试被问:编写 Java 程序时, 如何在 Java 中创建死锁并修复它?
这是一个经典的并发编程面试题,考察你对死锁原理、排查和解决的全面理解。我将从创建、诊断、修复三个维度详细拆解。
一、如何“制造”一个死锁
1. 核心原理回顾
死锁发生的四个必要条件(Coffman条件):
-
互斥:资源不能共享,一次只能被一个线程使用
-
占有且等待:线程持有资源并等待其他资源
-
不可剥夺:资源只能由持有线程主动释放
-
循环等待:存在一个线程等待环:T1等T2,T2等T3,...,Tn等T1
2. 经典死锁代码示例
java
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 持有 lock1,等待 lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 同时持有 lock1 和 lock2");
}
}
}, "线程-A");
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 持有 lock2,等待 lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 同时持有 lock1 和 lock2");
}
}
}, "线程-B");
t1.start();
t2.start();
}
}
执行结果:
text
线程-A 持有 lock1,等待 lock2 线程-B 持有 lock2,等待 lock1 (程序永远挂起,形成死锁)
二、如何在生产环境中诊断死锁
1. 使用JDK工具检测
java
// 主动检测死锁并打印信息
public static void checkDeadlock() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.findDeadlockedThreads();
if (threadIds != null && threadIds.length > 0) {
System.out.println("发现死锁线程:");
ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds);
for (ThreadInfo info : threadInfos) {
System.out.println(info.getThreadName() + " 在等待锁: "
+ info.getLockInfo() + ",被 " + info.getLockOwnerName() + " 持有");
}
}
}
2. 命令行诊断
bash
# 1. 查找Java进程ID jps -l # 2. 生成线程转储(多次执行以观察变化) jstack <pid> > thread_dump.txt # 3. 直接使用jcmd jcmd <pid> Thread.print
线程转储中的关键信息:
text
Found one Java-level deadlock: ============================= "线程-B": waiting to lock monitor 0x00007f8a4c008b00 (object 0x000000076ab7c7d8, a java.lang.Object), which is held by "线程-A" "线程-A": waiting to lock monitor 0x00007f8a4c00a400 (object 0x000000076ab7c7e8, a java.lang.Object), which is held by "线程-B"
3. 使用可视化工具
-
JConsole:连接进程 → 线程选项卡 → 检测死锁
-
VisualVM:更强大的线程分析功能
-
Arthas(阿里开源):
thread -b直接查找死锁
三、如何修复和预防死锁
策略1:破坏循环等待条件(最常用)
方案:统一锁的获取顺序
java
public class FixedDeadlock {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> acquireLocks(lock1, lock2), "线程-A");
Thread t2 = new Thread(() -> acquireLocks(lock1, lock2), "线程-B");
t1.start();
t2.start();
}
// 关键:统一按固定顺序获取锁
private static void acquireLocks(Object firstLock, Object secondLock) {
// 确定统一的顺序(例如通过hashCode比较)
Object lockA = firstLock.hashCode() < secondLock.hashCode() ? firstLock : secondLock;
Object lockB = lockA == firstLock ? secondLock : firstLock;
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + " 持有 " + lockA);
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " 持有 " + lockA + " 和 " + lockB);
}
}
}
}
策略2:破坏占有且等待条件
方案:一次性申请所有资源(All-or-None)
java
public class ResourceManager {
private final List<Object> locks = Arrays.asList(new Object(), new Object());
public boolean acquireLocks() {
// 尝试一次性获取所有锁
synchronized (locks.get(0)) {
if (!tryLock(locks.get(1))) {
// 获取失败则释放已持有的锁
return false;
}
return true;
}
}
private boolean tryLock(Object lock) {
// 实现非阻塞尝试获取锁
// 实际可使用ReentrantLock.tryLock()
return false; // 简化示例
}
}
策略3:使用超时机制(破坏不可剥夺条件)
方案:使用Lock接口替代synchronized
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void acquireLocksWithTimeout() throws InterruptedException {
while (true) {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获得 lock1");
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println("成功获取两个锁");
return; // 成功获取所有锁
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock(); // 释放第一个锁
}
}
// 获取失败,随机回退避免活锁
Thread.sleep((long) (Math.random() * 100));
}
}
}
策略4:使用无锁数据结构
java
// 使用并发容器避免显式锁
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeExample {
private final ConcurrentHashMap<String, AtomicInteger> map =
new ConcurrentHashMap<>();
public void update(String key, int delta) {
map.compute(key, (k, v) -> {
if (v == null) return new AtomicInteger(delta);
v.addAndGet(delta);
return v;
});
}
}
四、最佳实践与架构层面预防
1. 代码规范
java
// 使用SonarQube等工具检测潜在死锁
public class LockOrderChecker {
// 反模式:不同方法中以不同顺序获取锁
public void methodA() {
synchronized (lock1) {
synchronized (lock2) { /* ... */ }
}
}
public void methodB() {
synchronized (lock2) { // ❌ 与methodA顺序相反
synchronized (lock1) { /* ... */ }
}
}
}
2. 使用线程池监控
java
ExecutorService executor = Executors.newFixedThreadPool(5);
// 监控任务执行时间,长时间卡住可能死锁
executor.submit(() -> {
Future<?> future = executor.submit(task);
try {
future.get(5, TimeUnit.SECONDS); // 设置超时
} catch (TimeoutException e) {
future.cancel(true); // 中断可能死锁的任务
log.warn("任务执行超时,可能发生死锁");
}
});
3. 分布式环境下的死锁预防
java
// 使用数据库行锁时,统一按主键排序更新
public void updateMultipleRecords(List<Long> ids) {
ids.sort(Long::compareTo); // 统一排序
for (Long id : ids) {
// 按排序后的顺序更新
updateRecord(id);
}
}
五、面试回答要点
被问到时可以这样回答:
-
先解释原理:
“死锁需要满足四个条件,我会通过创建两个线程,让它们以相反顺序获取两个锁来演示。” -
展示代码:
“这是经典的交叉锁获取场景,线程A先锁1后锁2,线程B先锁2后锁1。” -
演示诊断方法:
“可以用jstack或ThreadMXBean.findDeadlockedThreads()来检测。” -
提出解决方案:
“预防死锁最常见的方法是破坏循环等待条件,也就是统一锁的获取顺序。另外,使用ReentrantLock的tryLock()设置超时也是生产环境的常用做法。” -
扩展讨论:
“在微服务架构中,分布式死锁更复杂,通常通过事务超时、Saga模式或使用支持死锁检测的数据库(如InnoDB)来处理。”
六、完整可运行示例
java
/**
* 死锁创建、检测、修复完整示例
*/
public class DeadlockCompleteDemo {
static class DeadlockCreator {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
void createDeadlock() {
new Thread(() -> {
synchronized (lock1) {
sleep(100);
synchronized (lock2) {
System.out.println("Thread1 got both locks");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
sleep(100);
synchronized (lock1) {
System.out.println("Thread2 got both locks");
}
}
}).start();
}
}
static class DeadlockFixer {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
void fixedVersion() {
new Thread(() -> acquireInOrder(lock1, lock2)).start();
new Thread(() -> acquireInOrder(lock1, lock2)).start();
}
private void acquireInOrder(Object a, Object b) {
// 通过hashCode确定固定顺序
Object first = System.identityHashCode(a) < System.identityHashCode(b) ? a : b;
Object second = first == a ? b : a;
synchronized (first) {
synchronized (second) {
System.out.println(Thread.currentThread().getName() + " acquired locks in order");
}
}
}
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 创建死锁 ===");
new DeadlockCreator().createDeadlock();
Thread.sleep(2000);
System.out.println("\n=== 修复版本 ===");
new DeadlockFixer().fixedVersion();
}
}
关键收获:
-
死锁是可以稳定复现的,不是随机bug
-
修复的核心是打破四个必要条件中的至少一个
-
统一锁顺序是最简单有效的预防策略
-
生产环境必须有死锁检测和恢复机制
这个问题的掌握程度,能直接体现你对并发编程的实战经验和系统思维。
更多推荐


所有评论(0)