一文搞懂死锁,手把手教你如何解

150 阅读6分钟

想象一下你和你的朋友去一家只有一把钥匙的咖啡厅。你需要这把钥匙去使用洗手间,而你的朋友需要同一把钥匙去取车。如果你持有钥匙不去洗手间,同时你的朋友也在等着你用完洗手间来取车,这就形成了一个僵局,因为你们都在等对方采取行动。这种情况就很像计算机科学中的“死锁”。

什么是死锁?

死锁是指两个或更多的进程(或线程)在执行过程中,因为争夺资源而造成的一种相互等待的现象,如果没有外力干预,它们都将无法继续进行。

死锁发生的常见场景

死锁经常发生在需要争夺有限资源的环境中,特别是在并发环境下,其中多个进程或线程同时运行,相互依赖资源。以下是一些死锁经常出现的典型场景:

  1. 操作系统:最常见的死锁环境。进程在执行过程中,可能会请求多种资源,如打印机、文件、内存等。当多个进程以不同的顺序请求相同的资源集时,很容易发生死锁。

  2. 数据库:事务处理中也容易发生死锁。比如,两个或多个事务分别锁定了对方需要的资源,并等待对方释放资源,从而形成死锁。

  3. 多线程程序:在任何多线程应用程序中,如果线程需要以不一致的顺序访问多个锁或资源,就可能发生死锁。这在进行复杂的数据处理或操作共享资源时尤其常见。

  4. 网络系统:在网络协议中,特别是在分布式系统中,死锁可能发生在请求和响应过程中,当两个或多个节点等待对方响应时,可能导致整个系统的停滞。

  5. 文件系统:多个应用程序可能会尝试同时访问和锁定文件系统中的文件,如果没有适当的锁管理机制,就可能导致死锁。

具体案例:

  • 操作系统中的资源分配:进程A持有资源1并请求资源2,而进程B持有资源2并请求资源1。如果这两个资源都不可剥夺,那么进程A和B都会无限期地等待对方释放资源,从而形成死锁。

  • 数据库中的事务处理:事务A锁定了表1并试图锁定表2,而事务B锁定了表2并试图锁定表1。这种情况下,两个事务都无法继续执行下去,除非其中一个事务被回滚。

  • 多线程程序中的锁顺序:假设有两个线程,线程1首先锁定资源A然后尝试锁定资源B,同时线程2首先锁定资源B然后尝试锁定资源A。如果线程1和线程2几乎同时运行,它们都在等待对方释放锁,就会导致死锁。

死锁形成的必要条件

  1. 互斥条件:资源不能被共享,只能由一个进程在任一时刻使用。就像咖啡厅里只有一把钥匙。
  2. 占有和等待条件:一个进程至少持有一个资源,并等待获取更多的资源,而这些资源被其他进程占有。就像你拿着钥匙等着用洗手间,你的朋友等着用同一把钥匙取车。
  3. 非剥夺条件:资源不能被强制从一个进程中移走,只能由占有资源的进程主动释放。意味着没有人能强制从你手中拿走钥匙。
  4. 循环等待条件:存在一种进程资源的循环等待链,形成闭环。比如,你等你的朋友,你的朋友等你。

如何避免死锁?

避免死锁的手段通常涉及系统设计和运行时策略的改进,以确保不会同时满足死锁的四个必要条件(互斥、占有和等待、非剥夺、循环等待)。下面是一些避免死锁的常见方法:

  1. 资源分配策略改进

    • 预防策略:通过破坏死锁的四个必要条件中的至少一个来预防死锁。例如,通过确保系统中至少有一个资源的分配方式不会导致循环等待(破坏循环等待条件)。
    • 避免策略:在分配资源之前判断这次分配是否安全,即是否有可能导致死锁。银行家算法是一种著名的资源分配避免策略,它通过避免分配会导致系统进入不安全状态的资源请求来预防死锁。
  2. 锁顺序

    • 确保所有进程按照相同的顺序请求资源,这样可以破坏循环等待的条件,因为循环等待需要一个环形链的形成,而统一的请求顺序避免了环形链的生成。
  3. 超时

    • 进程尝试获得资源时设置超时限制。如果进程在指定时间内未能获得所有所需资源,则释放已占有的资源并重新尝试。这种方法不能完全避免死锁,但可以帮助检测和恢复。
  4. 死锁检测和恢复

    • 系统不尝试避免死锁,而是允许死锁的发生,但会定期检测死锁或在检测到系统运行缓慢时进行检测。一旦检测到死锁,系统会通过某种机制恢复,比如终止一个或多个进程,或者剥夺一些资源。
  5. 使用非阻塞算法

    • 设计算法时,使用非阻塞的同步机制,如锁自旋等待,这样即使发生资源竞争,进程也不会进入阻塞状态,从而避免了死锁。这种方法更多地适用于低级的并发控制。
  6. 请求所有资源同时分配

    • 在进程开始执行之前,一次性请求所有需要的资源。这样可以避免占有和等待条件,因为进程在开始执行前已经拥有了所有必需的资源。

这些策略可以单独使用,也可以组合使用,以降低或消除系统中发生死锁的可能性。每种方法都有其适用场景和潜在的缺点,因此在选择特定策略时需要考虑系统的具体需求和约束。