什么是死锁?

Posted

技术标签:

【中文标题】什么是死锁?【英文标题】:What is a deadlock? 【发布时间】:2010-09-07 06:20:19 【问题描述】:

在编写多线程应用程序时,最常见的问题之一是死锁。

我向社区提出的问题是:

    如何检测它们?

    你会处理它们吗?

    最后,您如何防止它们发生?

【问题讨论】:

You first, my dear. 【参考方案1】:

当多个进程试图同时访问同一个资源时,就会发生

一个进程失败,必须等待另一个进程完成。

死锁发生在等待进程仍在等待第一个进程完成之前需要的另一个资源时。

举个例子:

资源A和资源B被进程X和进程Y使用

X 开始使用 A。 X 和 Y 尝试开始使用 B Y '获胜'并首先获得 B 现在 Y 需要使用 A A 被 X 锁定,正在等待 Y

避免死锁的最好方法是避免进程以这种方式交叉。尽可能减少锁定任何东西的需要。

在数据库中避免在单个事务中对不同表进行大量更改,避免触发器并尽可能切换到乐观/脏/无锁读取。

【讨论】:

我在这里使用进程作为概括,而不是专门使用操作系统进程。这些可能是线程,但也可能是完全不同的应用程序或数据库连接。模式是一样的。 您好,在这种情况下:线程 A 锁定资源 A 并且进程很长。线程 B 等待锁定资源 A。CPU 时间占用:20%,你能认为这是死锁情况吗? @rickyProgrammer 不,这只是一个常规的锁等待,尽管区别有点学术性。 B 等待慢 A 是锁,B 等待 A 等待 B 是死锁。 所以死锁更多的是两个进程的锁定资源等待这些资源被释放.. @rickyProgrammer 这是一个不会释放的锁,无论你等待多久,因为循环队列。【参考方案2】:

当两个线程获得阻止其中任何一个进程的锁时,就会发生死锁。避免它们的最好方法是仔细开发。许多嵌入式系统通过使用看门狗定时器(当系统挂起一定时间时重置系统的定时器)来防止它们。

【讨论】:

【参考方案3】:

当线程正在等待从未发生过的事情时,就会发生死锁。

通常情况下,当线程正在等待先前所有者从未释放的互斥锁或信号量时会发生这种情况。

当您遇到这样的涉及两个线程和两个锁的情况时,也经常发生这种情况:

Thread 1               Thread 2

Lock1->Lock();         Lock2->Lock();
WaitForLock2();        WaitForLock1();   <-- Oops!

您通常会检测到它们,因为您期望发生的事情永远不会发生,或者应用程序完全挂起。

【讨论】:

当线程正在等待不能发生的事情时发生死锁。【参考方案4】:

只有当您有两个或多个可以同时获取的锁并且以不同的顺序获取它们时才会发生死锁。

避免死锁的方法是:

避免使用锁(如果可能), 避免拥有多个锁 始终按相同的顺序使用锁。

【讨论】:

防止死锁的第三点(总是以相同的顺序获取锁)至关重要,这在编码实践中很容易被遗忘。【参考方案5】:

死锁是系统的一种状态,其中没有单个进程/线程能够执行操作。正如其他人所提到的,死锁通常是由于每个进程/线程都希望获得已被另一个(甚至相同)进程/线程锁定的资源的锁。

有多种方法可以找到它们并避免它们。一种是非常努力地思考和/或尝试很多事情。然而,处理并行性是出了名的困难,大多数(如果不是全部)人将无法完全避免问题。

如果您认真对待这类问题,一些更正式的方法会很有用。我知道的最实用的方法是使用过程理论方法。在这里,您可以使用某种过程语言(例如 CCS、CSP、ACP、mCRL2、LOTOS)对系统进行建模,并使用可用的工具来(模型)检查死锁(可能还有其他一些属性)。使用的工具集示例有 FDR、mCRL2、CADP 和 Uppaal。一些勇敢的人甚至可能通过使用纯符号方法(定理证明;寻找 Owicki-Gries)来证明他们的系统没有死锁。

但是,这些形式化方法通常确实需要一些努力(例如学习过程理论的基础知识)。但我想这只是这些问题很难解决的结果。

【讨论】:

【参考方案6】:

您可以在 Deadlock 部分查看此wonderful articles。它在 C# 中,但其他平台的想法仍然相同。我在这里引用以方便阅读

当两个线程各自等待一个由 另一个,所以两者都不能继续。说明这一点的最简单方法 有两把锁:

object locker1 = new object();
object locker2 = new object();

new Thread (() => 
                    lock (locker1)
                    
                      Thread.Sleep (1000);
                      lock (locker2);      // Deadlock
                    
                  ).Start();
lock (locker2)

  Thread.Sleep (1000);
  lock (locker1);                          // Deadlock

【讨论】:

【参考方案7】:

Mutex 本质上是一个锁,提供对共享资源的受保护访问。在 Linux 下,线程互斥数据类型是 pthread_mutex_t。使用前先初始化一下。

要访问共享资源,您必须锁定互斥锁。如果互斥锁已经上锁,则调用将阻塞线程,直到互斥锁解锁。完成对共享资源的访问后,您必须解锁它们。

总的来说,有一些不成文的基本原则:

在使用共享资源前获取锁。

保持锁定的时间尽可能短。

如果线程返回错误则释放锁。

【讨论】:

这描述的是锁,而不是死锁。【参考方案8】:

当存在一个循环的线程或进程链,每个线程或进程都持有一个锁定的资源并试图锁定链中下一个元素持有的资源时,就会发生死锁。例如,两个线程分别持有锁 A 和锁 B,并且都试图获取另一个锁。

【讨论】:

我投票给你。您的回答比上面更简洁,因为它们使进程或线程发生混淆死锁。有人说进程,有人说线程:)【参考方案9】:

死锁是当不同进程请求的可用资源数量较少时发生的情况。这意味着当可用资源的数量少于用户请求的数量时,此时进程进入等待状态。有时等待增加更多,没有机会检查资源不足的问题这种情况称为死锁。 实际上,死锁对我们来说是一个主要问题,它只发生在多任务操作系统中。死锁不会发生在单任务操作系统中,因为所有资源都只存在于当前正在运行的那个任务......

【讨论】:

【参考方案10】:

要定义死锁,首先我要定义进程。

进程 我们知道进程只不过是一个正在执行的program

资源 执行一个程序进程需要一些资源。资源类别可能包括内存、打印机、CPU、打开的文件、磁带驱动器、CD-ROM 等。

死锁 死锁是两个或多个进程同时持有一些资源并试图获取更多资源,直到它们无法释放资源的情况或条件。他们在那里完成执行。

死锁条件或情况

上图中有两个进程P1p2,有两个资源R1R2 .

资源R1分配给进程P1,资源R2分配给进程p2。 为了完成进程P1的执行需要资源R2,所以P1请求R2,但是R2 已分配给 P2

同理P2进程需要R1完成其执行,但R1已经分配给P1 >.

两个进程都不能释放它们的资源,除非它们完成它们的执行。所以两者都在等待另一个资源,他们将永远等待。所以这是一个DEADLOCK条件。

为了发生死锁,必须满足四个条件。

    互斥 - 每个资源要么当前分配给一个进程,要么可用。 (两个进程不能 同时控制相同的资源或处于他们的关键 部分)。 持有并等待 - 当前持有资源的进程可以请求新资源。 无抢占 - 一旦一个进程持有资源,它就不能被另一个进程或内核夺走。 循环等待 - 每个进程都在等待获取由另一个进程持有的资源。

所有这些条件都在上图中得到满足。

【讨论】:

【参考方案11】:

死锁是操作系统中多处理/多道程序问题中的常见问题。 假设有两个进程 P1、P2 和两个全局共享资源 R1、R2,并且在关键部分需要访问这两个资源

最初,操作系统将 R1 分配给进程 P1,将 R2 分配给进程 P2。 由于两个进程同时运行,它们可能会开始执行它们的代码,但是当进程到达临界区时就会出现问题。 所以进程 R1 将等待进程 P2 释放 R2,反之亦然...... 所以他们将永远等待(死锁条件)。

一个小类比……

你的母亲(操作系统), 你(P1), 你的兄弟(P2), 苹果(R1), 刀(R2), 关键部分(用刀切苹果)。

一开始你妈妈把苹果和刀给了你弟弟。 两人都很开心并且在玩(执行他们的代码)。 你们中的任何人都想在某个时候切苹果(关键部分)。 你不想把苹果给你哥哥。 你哥哥不想把刀给你。 所以你们两个都要等很久很久了:)

【讨论】:

【参考方案12】:

让我解释一下犯罪电影中的僵局情况的真实世界(实际上不是真实的)示例。想象一个罪犯劫持了人质,而警察也劫持了一个人质,而人质是罪犯的朋友。在这种情况下,如果警察不让他的朋友放手,罪犯就不会放人质。除非罪犯释放人质,否则警察不会放过罪犯的朋友。这是一个无休止的不信任局面,因为双方都在坚持从对方迈出第一步。

犯罪和警察场景

简单来说,当两个线程需要两个不同的资源并且每个线程都拥有另一个需要的资源的锁时,这就是死锁。

死锁的另一个高级解释:破碎的心

你和一个女孩约会,吵架后的一天,双方都心碎,等待我很抱歉,我很想你电话.在这种情况下,当且仅当其中一方收到另一方的 I-am-sorry 呼叫时,双方都希望相互交流。因为双方都不会开始通信并处于被动状态等待,所以双方都会等待对方开始通信,最终陷入死锁状态。

【讨论】:

线程应该属于不同的进程吗?,属于同一个进程的线程也会导致死锁吗? @diabolicfreak 线程是否属于同一个进程并不重要。 现实生活中的另一个例子可能是四辆汽车同时从四个方向穿过两条相等的道路。每个人都需要从右侧给汽车让路,所以没有人可以继续前进。 那些现实生活中的例子非常具有描述性,也很有趣。 另一个“现实生活”示例:The dining philosophers【参考方案13】:

上面的一些解释很好。希望这也可能有用: https://ora-data.blogspot.in/2017/04/deadlock-in-oracle.html

在数据库中,当一个会话(例如 ora)想要另一个会话(例如数据)持有的资源,但该会话(数据)也想要由第一个会话(ora)持有的资源。也可能涉及 2 个以上的会话,但想法是相同的。 实际上,死锁会阻止某些事务继续工作。 例如: 假设,ORA-DATA 持有锁 A 并请求锁 B SKU持有锁B,请求锁A。

谢谢,

【讨论】:

【参考方案14】:

当一个线程等待其他线程完成时会发生死锁,反之亦然。

如何避免? - 避免嵌套锁 - 避免不必要的锁 - 使用线程join()

您如何检测到它? 在 cmd 中运行此命令:

jcmd $PID Thread.print

reference : geeksforgeeks

【讨论】:

【参考方案15】:

一个经典且非常简单的理解死锁情况的程序:-

public class Lazy 

    private static boolean initialized = false;

    static 
        Thread t = new Thread(new Runnable() 
            public void run() 
                initialized = true;
            
        );

        t.start();

        try 
            t.join();
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

    public static void main(String[] args) 
        System.out.println(initialized);
    

当主线程调用 Lazy.main 时,它会检查类 Lazy 已经初始化并开始初始化类。这 主线程现在将 initialized 设置为 false ,创建并启动背景 run 方法设置为 true 的线程,并等待后台线程完成。

这一次,该类当前正在由另一个线程初始化。 在这种情况下,当前线程,也就是后台线程, 等待 Class 对象,直到初始化完成。不幸的是,线程 也就是在做初始化,主线程,正在等待后台 线程来完成。因为这两个线程现在正在互相等待,所以 程序死锁。

【讨论】:

【参考方案16】:

死锁不仅仅发生在锁上,尽管这是最常见的原因。在 C++ 中,您可以通过让每个线程在 std::thread 对象上为另一个线程调用 join() 来创建具有两个线程且没有锁的死锁。

【讨论】:

【参考方案17】:

基于锁的并发控制

使用锁定来控制对共享资源的访问很容易出现死锁,而仅靠事务调度器无法阻止它们的发生。

例如,关系数据库系统使用各种锁来保证事务ACID 属性。

无论您使用什么关系数据库系统,在修改(例如,UPDATEDELETE)某个表记录时,总是会获取锁。如果不锁定由当前正在运行的事务修改的行,Atomicity 将受到损害)。

什么是死锁

当两个并发事务无法进行时,就会发生死锁,因为每个事务都在等待另一个释放锁,如下图所示。

因为两个事务都处于锁获取阶段,所以没有一个事务在获取下一个之前释放锁。

从死锁情况中恢复

如果您使用依赖于锁的并发控制算法,则始终存在陷入死锁情况的风险。死锁可能发生在任何并发环境中,而不仅仅是在数据库系统中。

例如,如果两个或多个线程正在等待先前获得的锁,则多线程程序可能会死锁,因此没有线程可以取得任何进展。如果这发生在 Java 应用程序中,JVM 不能仅仅强制线程停止执行并释放其锁。

即使Thread 类公开了stop 方法,该方法自Java 1.1 以来已被弃用,因为它可能导致对象在线程停止后处于不一致状态。取而代之的是,Java 定义了一个 interrupt 方法,它作为一个提示,因为被中断的线程可以简单地忽略中断并继续执行。

因此,Java 应用程序无法从死锁情况中恢复,应用程序开发人员有责任以不会发生死锁的方式对锁获取请求进行排序。

但是,数据库系统无法强制执行给定的锁获取顺序,因为无法预见某个事务将进一步获取哪些其他锁。保持锁定顺序成为数据访问层的职责,数据库只能协助从死锁情况中恢复。

数据库引擎运行一个单独的进程,该进程扫描当前冲突图的锁等待周期(由死锁引起)。 当检测到一个循环时,数据库引擎会选择一个事务并中止它,导致它的锁被释放,以便另一个事务可以进行。

与 JVM 不同,数据库事务被设计为一个原子工作单元。因此,回滚会使数据库保持一致状态。

【讨论】:

以上是关于什么是死锁?的主要内容,如果未能解决你的问题,请参考以下文章

操作系统-死锁死锁发生的条件是什么?死锁的避免和预防方法

什么是死锁?如果避免死锁?

什么是死锁?死锁产生的原因?

什么是死锁,简述死锁发生的四个必要条件,如何避免与预防死锁

一篇文章搞清JVM死锁问题及排查

死锁面试题(什么是死锁,产生死锁的原因及必要条件)(*)