程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

什么是死锁详细介绍一下在哪些情况下会发生死锁?

balukai 2025-02-10 11:22:14 文章精选 5 ℃

死锁(Deadlock)是在多线程编程场景下常见的一种并发问题。是指在两个或者是多个线程的同时执行的过程中,因为资源竞争而导致的一种线程相互之间等待的状态,最终导致的情况就是每个线程都无法正常执行,程序无法正常结束,出现影响用户体验影响系统性能等问题,严重点的可能会导致系统宕机。

产生死锁的条件

??根据现代软件工程学总结的产生死锁问题,需要满足如下的四个条件。

互斥条件

??满足互斥条件的前提就是至少存在一个"非共享"状态的资源,只允许每次一个线程对该资源进行操作,如果其他线程想要操作这个资源,那么就必须要等待占用资源的线程操作结束才可以进行操作。例如如果有一个线程A占用了锁A,那么如果有其他线程想要获取锁A,就必须要等待线程A释放了锁A之后才可以进行获取。

占有且等待

??假设有一个线程A持有了锁A,这个时候线程A如果想要尝试获取锁B,但是它不释放自己占用的锁A,这种情况下,如果有线程B想要尝试获取锁A的话,由于锁B被其他线程占用,无法获取线程A就无法释放锁A,就导致线程B无法获取锁A,无法执行后续的操作,从而造成死锁。

不剥夺条件

??如果一个线程获取到了某个资源,就不允许其他线程强行剥夺该资源的使用权,必须要等待获取到资源的线程对资源操作完成之后自己释放了该资源后其他线程才可以操作该资源。在过程中如果资源持有线程不释放资源的话,其他线程是没有办法强行占用该资源的使用权。

循环等待

??在系统资源调用中存在了一个线程调用链路,并且在链路中的每个资源都在等待上一个线程释放现有持有资源,从而就会形成一个资源调用的闭环。例如线程A持有锁A,尝试获取锁B,线程B持有锁B,尝试获取锁C,线程C持有锁C,尝试获取锁A。这样就会形成一个闭环最终导致死锁问题。

??当这四个条件同时满足的时候,就会导致死锁问题的出现。

产生死锁的业务场景

??产生死锁问题的场景有很多,但是大多数情况都是发生在多线程竞争共享资源的时候,如果没有对共享资源做适当的资源管理策略,或者是没有对线程做同步调用策略,那么很有可能就会导致死锁问题的产生。如下所示。

嵌套锁竞争

??当多个线程持有不同的资源锁的时候,还要尝试获取其他线程持有的锁,在这种情况下,由于满足了占有且等待条件和互斥条件,所以就有可能会发生死锁。

??例如,如下所示,线程A持有锁A,线程B持有锁B,线程A请求锁B,线程B请求锁A。这种情况下两个线程互相等待,最终造成死锁。

class ThreadA extends Thread {
    private final Object lockA;
    private final Object lockB;

    public ThreadA(Object lockA, Object lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    public void run() {
        synchronized (lockA) {
            System.out.println("Thread A acquired lockA");
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockB) {
                System.out.println("Thread A acquired lockB");
            }
        }
    }
}

class ThreadB extends Thread {
    private final Object lockA;
    private final Object lockB;

    public ThreadB(Object lockA, Object lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    public void run() {
        synchronized (lockB) {
            System.out.println("Thread B acquired lockB");
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockA) {
                System.out.println("Thread B acquired lockA");
            }
        }
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();

        ThreadA threadA = new ThreadA(lockA, lockB);
        ThreadB threadB = new ThreadB(lockA, lockB);

        threadA.start();
        threadB.start();
    }
}

??在上面的代码实现中,ThreadAThreadB分别持有锁A和锁B并尝试获取对方持有的锁,这样就造成了满足互斥、占有且等待的条件,从而形成了死锁。

资源请求的顺序不当

??在有些场景下,在多个线程对某写资源进行操作的时候,由于对资源顺序要求不一致,就有可能会导致死锁问题,也就是说如果线程1按顺序请求资源A、B,线程2按顺序请求资源B、A,那么两者可能互相等待对方释放资源,从而造成死锁。如下所示。

  • 线程1:请求锁A -> 请求锁B
  • 线程2:请求锁B -> 请求锁A

??这种情况下,由于线程1获取了锁A,并等待锁B;线程2获取了锁B,并等待锁A,最终形成了循环等待。就满足了上面四个条件中的不剥夺和循环等待的问题。

嵌套锁和条件变量

??在一些复杂的应用中,可能会通过条件变量与锁的结合来进行线程同步的控制,当线程在等待条件变量时持有锁,而同时其他线程也需要同一个条件变量的时候,这种情况下就会导致死锁问题发生。例如线程A持有锁A并等待条件1,线程B持有锁B并等待条件2,而线程A需要条件2,线程B需要条件1,这就可能发生死锁。

锁的粒度过大

??如果一个线程在持有某个大锁时,去请求其他小锁,这也可能造成死锁。尤其是当多个线程在不同的锁上存在依赖关系的时候,这种情况下,如果发生了这种情况之后,就很容器造成死锁。

如何避免死锁

??为了避免发生死锁,我们需要按照上面产生死锁的条件来提供对应的解决办法,就可以有效的解决死锁问题,如下所示,是一些常见的死锁解决方法。

避免锁嵌套

??在系统设计之初,需要尽量避免出现一个线程在持有某个锁的同时尝试请求其他锁。对于涉及到了多个资源操作的玩呢提,可以将获取锁的顺序进行固定,确保所有的线程都按照同样的顺序来请求锁。

锁顺序

??确保在所有线程获取到多个锁的时候,能够遵循一定的顺序来进行获取,例如线程A和线程B都必须按照锁A、锁B的顺序来请求锁。这样即使两个线程需要相同的资源,也不会形成循环等待。

尝试锁(TryLock)

??在我们使用ReentrantLock等显式锁调用的时候,我们可以尝试通过tryLock()方法来尝试获取锁,避免线程一直在阻塞等待获取锁,因为这种方式会设置一个超时时间,所以就可以避免线程一直等待导致死锁。

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

if (lockA.tryLock() && lockB.tryLock()) {
    try {
        // 进行操作
    } finally {
        lockA.unlock();
        lockB.unlock();
    }
}

使用超时机制

??在请求多个锁的时候,我们可以为请求锁设置一个超时时间。也就是说当一个线程在超时之前无法获得所有锁,就让它释放已经获得的锁,提供给其他线程调用,在等待一段时间之后再重新尝试获取锁。通过这种方式来防止死锁情况的发生。

死锁检测

??可以通过定期检查系统中是否发生了死锁的情况来避免死锁问题的产生。例如在某些高级并发控制中,使用死锁检测算法来识别并解决死锁。

总结

??死锁问题是在并发编程中一个非常重要且不可被忽略的问题,一般,会发生在多个线程相互竞争多个资源的时候。并且当死锁的四个条件同时满足的时候,系统就会进入死锁状态。而我们为了避免死锁的发生,可以通过合理设计锁的获取顺序、避免嵌套锁、使用超时机制和tryLock()等方式加以避免。

最近发表
标签列表