Skip to content

线程死锁的问题

什么是死锁

死锁是多个线程在运行过程中因争夺资源而相互等待,导致“永久”阻塞的一种情况。

预防和解决死锁的方法包括:

  1. 资源排序: 保证所有线程按照相同的顺序请求资源,这样可以避免循环等待的条件,从而预防死锁。列如A、B两个资源,为每个资源分配一个唯一的序号,线程必须按照序号的升序请求资源,并按照序号的降序释放资源。通过这样的方式可以保证线程不会行程循环,从而避免了死锁。

  2. 超时机制:为线程获取资源的操作设置超时时间,超时后线程可以放弃等待,回退并重新尝试,这样可以避免线程永久阻塞。

  3. 死锁检测与恢复: 使用算法定期检测系统是否进入死锁状态,一旦检测到死锁,通过强制释放某些资源或终止某些线程来恢复系统正常运行。

预防死锁的关键是合理的设计资源分配策略和线程调度机制,确保系统在运行过程中不会出现不可解的资源竞争。

死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个线程使用。

  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:线程已获得的资源在未使用完之前,不能强制剥夺。

  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

避免死锁

  1. 破坏互斥条件: 几乎无法做到,因为互斥是多线程环境下资源共享的基本要求。

  2. 破坏请求与保持条件: 避免一个线程同时持有多个资源,可以在一个线程开始时一次性请求它所需要的所有资源。

  3. 破坏不剥夺条件: 当一个线程请求资源得不到满足时,必须释放已获得的资源。

  4. 破坏循环等待条件: 可以通过对资源进行有序分配来避免循环等待。

面试题:如何避免死锁

  1. 可以设置超时机制,避免死锁
  2. 通过对资源排序避免死锁
  3. 还有一种死锁,是同一线程重复获取同一资源的死锁,可以使用可重入锁来避免这种情况。

排查死锁

1. 线程转储分析(Thread Dump Analysis):

  • 使用 jstack 工具获取 Java 进程的线程转储文件。
  • 分析线程转储文件,寻找 Deadlock 标记,确定哪些线程以及哪些资源导致了死锁。
bash
jstack <pid> > threaddump.txt

2. 使用监控工具:

  • 使用 JConsole、VisualVM 等监控工具,它们可以实时监控线程的状态,帮助发现和分析死锁。
  • 这些工具可以展示线程的堆栈信息和锁持有情况。

3. 日志分析:

  • 在代码中加入详细的日志记录,特别是在获取锁、释放锁、线程等待等位置记录日志信息。
  • 通过分析日志,发现线程在何时何处陷入等待状态,从而推断出可能的死锁原因。

4. 定期检测:

  • 在代码中加入死锁检测逻辑,定期检测是否存在循环等待的情况。一旦检测到,可以采取相应的处理措施(如重启线程等)。

资源排序的实现案例

java
public class Resource {
    private final String name;

    public Resource(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class ResourceOrderingExample {
    private final Resource resource1 = new Resource("Resource1");
    private final Resource resource2 = new Resource("Resource2");

    public void acquireResourcesInOrder() {
        synchronized (resource1) {
            System.out.println(Thread.currentThread().getName() + " acquired " + resource1.getName());
            synchronized (resource2) {
                System.out.println(Thread.currentThread().getName() + " acquired " + resource2.getName());
                // Perform operations with both resources
            }
        }
    }

    public void acquireResourcesInReverseOrder() {
        synchronized (resource2) {
            System.out.println(Thread.currentThread().getName() + " acquired " + resource2.getName());
            synchronized (resource1) {
                System.out.println(Thread.currentThread().getName() + " acquired " + resource1.getName());
                // Perform operations with both resources
            }
        }
    }

    public static void main(String[] args) {
        ResourceOrderingExample example = new ResourceOrderingExample();

        Runnable task1 = () -> {
            example.acquireResourcesInOrder();
        };

        Runnable task2 = () -> {
            example.acquireResourcesInOrder();
        };

        Thread thread1 = new Thread(task1, "Thread-1");
        Thread thread2 = new Thread(task2, "Thread-2");

        thread1.start();
        thread2.start();
    }
}