在编写多线程应用程序时,最常遇到的问题之一是死锁。
我向社区提出的问题是:
什么是死锁?
如何检测它们?
您如何处理它们?
最后,您如何防止它们发生?
在编写多线程应用程序时,最常遇到的问题之一是死锁。
我向社区提出的问题是:
什么是死锁?
如何检测它们?
您如何处理它们?
最后,您如何防止它们发生?
锁定指的是多个进程同时尝试访问同一资源时发生的情况。
一个进程失败并必须等待另一个进程完成。
死锁发生在等待的进程仍然持有另一个进程需要才能完成的资源上。
所以,举个例子:
进程 X 和进程 Y 使用资源 A 和资源 B
避免死锁的最佳方法是尽量减少进程交叉使用资源的情况。尽可能减少锁定任何东西的需求。
在数据库中,避免在单个事务中对不同的表进行大量更改,避免触发器,并尽可能切换到乐观/脏/无锁读取。
让我举一个现实(实际上并不真实)的例子来解释死锁情况,这个例子来自于犯罪电影。想象一下,一个罪犯挟持了人质,而对此,一名警察也挟持了罪犯的朋友作为人质。在这种情况下,如果警察不放走罪犯的朋友,罪犯是不会放走人质的。同样地,除非罪犯释放人质,警察也不会放走罪犯的朋友。这是一种无休止的不信任局面,因为双方都坚持对方先采取行动。
简单来说,当两个线程需要两个不同的资源,并且它们中的每一个都拥有另一个需要的资源的锁时,就会发生死锁。
你和一个女孩约会,在一次争吵之后,双方对彼此心生怨恨,等待对方的“我很抱歉,我想念你”的电话。在这种情况下,双方都希望只有当其中一方收到了对方的“我很抱歉”电话时才能互相沟通。因为双方都不会主动开始沟通,而是被动地等待着对方开始沟通,最终陷入了死锁状态。
只有当您有两个或多个可以同时获取的锁并且它们以不同的顺序被抓取时,死锁才会发生。
避免死锁的方法包括:
首先,我要定义进程来解释死锁。
进程:进程实际上就是正在执行的程序
。
资源:进程执行程序需要一些资源。资源类别可能包括内存、打印机、CPU、打开的文件、磁带驱动器、CD-ROM等。
死锁:当两个或多个进程持有某些资源并尝试获取更多资源时,它们无法释放资源直到完成其执行,这种情况或条件称为死锁。
死锁条件或情况
在上图中,有两个进程P1和p2,还有两个资源R1和R2。
资源R1被分配给进程P1,资源R2被分配给进程p2。为了完成进程P1的执行,需要资源R2,因此P1请求R2,但是R2已经分配给了P2。
同样地,进程P2需要资源R1才能完成其执行,但是R1已经分配给了P1。
两个进程都无法释放其资源,直到它们完成执行为止。因此,它们都在等待另一个资源,将永远等待下去。所以这是一个死锁条件。
要发生死锁,必须满足四个条件。
并且上述图表中所有这些条件都得到满足。
死锁是指线程在等待从未发生的事情。
通常,当线程等待前一个所有者从未释放的互斥锁或信号量时,就会发生死锁。
当涉及到两个线程和两个锁的情况时,也经常会发生死锁,如下所示:
Thread 1 Thread 2
Lock1->Lock(); Lock2->Lock();
WaitForLock2(); WaitForLock1(); <-- Oops!
通常情况下,您会检测到它们,因为您期望发生的事情从未发生过,或者应用程序完全挂起。
当两个线程都等待彼此持有的资源时,就会发生死锁,因此两个线程都无法继续进行。 最容易用两个锁来说明:
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
}
使用锁来控制共享资源的访问容易出现死锁,而事务调度器本身无法防止其发生。
例如,关系型数据库系统使用各种锁来保证事务的 ACID
特性。
无论使用哪个关系型数据库系统,在修改(如UPDATE
或DELETE
)某个表记录时,都会始终获取锁。如果不锁定当前运行事务修改的行,则会破坏 原子性
)。
当两个并发事务由于彼此等待对方释放锁而无法进展时,即发生死锁,如下图所示。
因为两个事务都处于锁获取阶段,所以在获取下一个锁之前,它们都不会释放锁。
如果你正在使用依赖锁的并发控制算法,那么就存在遇到死锁的风险。死锁可以在任何并发环境中发生,不仅仅是在数据库系统中。
例如,如果两个或多个线程等待先前获取的锁,则多线程程序可能会发生死锁。如果这种情况发生在Java应用程序中,则JVM不能强制停止线程的执行并释放其锁。
即使Thread
类公开了一个stop
方法,但该方法已自Java 1.1起被弃用,因为它可能导致对象在线程停止后处于不一致的状态。相反,Java定义了一个interrupt
方法,作为提示,被中断的线程可以简单地忽略中断并继续其执行。
因此,Java应用程序无法从死锁情况中恢复,而是由应用程序开发人员负责按照特定的锁获取请求顺序,以确保死锁永远不会发生。
然而,数据库系统无法强制执行给定的锁获取顺序,因为不可能预见某个事务将要进一步获取哪些锁。保持锁顺序成为数据访问层的责任,数据库只能协助从死锁情况中恢复。
数据库引擎运行一个单独的进程来扫描当前的冲突图以查找锁等待循环(由死锁引起)。 当检测到循环时,数据库引擎选择一个事务并将其中止,导致其锁被释放,以便其他事务可以继续进行。
与JVM不同,数据库事务被设计为原子工作单元。因此,回滚会使数据库保持一致的状态。
当存在一组循环链的线程或进程时,每个线程或进程都持有一个锁资源并尝试锁定由链中下一个元素持有的资源时,就会发生死锁。例如,两个线程分别持有锁A和锁B,并且都试图获取另一个锁。
死锁是指两个线程都获得了锁,这阻止了它们中的任何一个继续进行。避免死锁的最好方法是仔细开发。许多嵌入式系统通过使用看门狗定时器(一种定时器,每当系统挂起一段时间时就会重置系统)来防止死锁。