这是一个经典的并发编程面试题,考察你对死锁原理、排查和解决的全面理解。我将从创建、诊断、修复三个维度详细拆解。


一、如何“制造”一个死锁

1. 核心原理回顾

死锁发生的四个必要条件(Coffman条件):

  1. 互斥:资源不能共享,一次只能被一个线程使用

  2. 占有且等待:线程持有资源并等待其他资源

  3. 不可剥夺:资源只能由持有线程主动释放

  4. 循环等待:存在一个线程等待环: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);
    }
}

五、面试回答要点

被问到时可以这样回答

  1. 先解释原理
    “死锁需要满足四个条件,我会通过创建两个线程,让它们以相反顺序获取两个锁来演示。”

  2. 展示代码
    “这是经典的交叉锁获取场景,线程A先锁1后锁2,线程B先锁2后锁1。”

  3. 演示诊断方法
    “可以用jstack或ThreadMXBean.findDeadlockedThreads()来检测。”

  4. 提出解决方案
    “预防死锁最常见的方法是破坏循环等待条件,也就是统一锁的获取顺序。另外,使用ReentrantLock的tryLock()设置超时也是生产环境的常用做法。”

  5. 扩展讨论
    “在微服务架构中,分布式死锁更复杂,通常通过事务超时、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

  • 修复的核心是打破四个必要条件中的至少一个

  • 统一锁顺序是最简单有效的预防策略

  • 生产环境必须有死锁检测和恢复机制

这个问题的掌握程度,能直接体现你对并发编程的实战经验和系统思维。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐