
字节一面:说说你了解的死锁?包括死锁产生原因、必要条件、处理方法、死锁回复以及死锁预防等(死锁相关问题大总结,超全!)
原创 程序员阿沛 程序员阿沛 [ 程序员阿沛 ](javascript:void(0);)
本文归于合集: [ 吊打面试官系列
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
** 面试官:说说你了解的死锁,和死锁产生原因。 **
举个例子:两个线程A和B,两个数据1和2。线程A在执行过程中,首先对资源1加锁,然后再去给资源2加锁,但是由于线程的切换,导致线程A没能给资源2加锁。线程切换到B后,线程B先对资源2加锁,然后再去给资源1加锁,由于资源1已经被线程A加锁,因此线程B无法加锁成功,当线程切换为A时,A也无法成功对资源2加锁,由此就造成了线程AB双方相互对一个已加锁资源的等待,死锁产生。
理论上认为死锁产生有以下四个必要条件,缺一不可:
a. 互斥条件
进程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个进程占用。
如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的进程释放。
b.请求与保持条件
进程已经保持至少一个资源,又请求新的资源而失败。 此时请求进程被阻塞,但对自己已获得的资源保持不放。
c. 不可抢占条件
进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时自己释放。
d. 循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系。
从资源竞争的角度来说,又可以分为 不可抢占资源竞争 和 可消耗资源竞争两种情况。
a. 不可抢占资源竞争
系统中所拥有的不可抢占性资源(如磁带机、打印机等)数量不足以满足多个进程运行的需要。 进程在运行过程中,会因争夺这些资源而陷入僵局。
b.可消耗资源竞争:
可消耗资源(如硬件中断、信号、消息、缓冲区内的消息等)也可能引起死锁。 这些资源由一个进程产生,被另一个进程使用,短时间后便无用。
产生死锁的具体原因如下。
1. 系统资源有限
当系统资源数量有限时,多个进程对资源的竞争更加激烈,更容易产生死锁。
2.资源分配策略不当
如果系统的资源分配策略不合理,也可能导致进程因竞争资源不当而产生死锁。
3.程序员编写的程序错误:
程序员在编写程序时,如果对资源的使用和释放处理不当,也可能导致死锁的发生。
** 面试官:请你写一段代码模拟一下死锁的发生。 **
** **
通过代码的形式进行演示,需要两个线程和两个互斥量。
#include <iostream>#include <vector>#include <list>#include <thread>#include <mutex> //引入互斥量头文件using namespace std;
class A {public: //插入消息,模拟消息不断产生 void insertMsg() { for (int i = 0; i < 100; i++) { cout << "插入一条消息:" << i << endl; my_mutex1.lock(); //语句1 my_mutex2.lock(); //语句2 Msg.push_back(i); my_mutex2.unlock(); my_mutex1.unlock(); } } //读取消息 void readMsg() { int MsgCom; for (int i = 0; i < 100; i++) { MsgCom = MsgLULProc(i); if (MsgLULProc(MsgCom)) { //读出消息了 cout << "消息已读出" << MsgCom << endl; } else { //消息暂时为空 cout << "消息为空" << endl; } } } //加解锁代码 bool MsgLULProc(int &command) { int curMsg; my_mutex2.lock(); //语句3 my_mutex1.lock(); //语句4 if (!Msg.empty()) { //读取消息,读完删除 command = Msg.front(); Msg.pop_front();
my_mutex1.unlock(); my_mutex2.unlock(); return true; } my_mutex1.unlock(); my_mutex2.unlock(); return false; }private: std::list<int> Msg; //消息变量 std::mutex my_mutex1; //互斥量对象1 std::mutex my_mutex2; //互斥量对象2};
int main() { A a; //创建一个插入消息线程 std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象 //创建一个读取消息线程 std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象 insertTd.join(); readTd.join(); return 0;}
语句1和语句2表示线程A先锁资源1,再锁资源2,语句3和语句4表示线程B先锁资源2再锁资源1,具备死锁产生的条件。
** 面试官:那么死锁该如何解决? **
可以从预防死锁、避免死锁、死锁检测、死锁恢复来解决。
a. 预防死锁
1. 破坏循环等待条件
锁排序:通过对加锁的操作进行排序,可以避免循环等待的发生。例如,在多线程编程中,可以按照资源在数组中的位置顺序,或数据库中表的顺序进行加锁操作。
资源有序分配法: 给系统中的资源编号,所有线程申请资源时必须按编号递增的顺序提出请求,这样就不会出现循环等待的情况。
2. 破坏请求保持条件
一次性分配所有资源:线程在运行前一次性申请完它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,线程暂不运行。这种方式虽然简单,但可能导致资源利用率降低,因为有些资源可能在线程的整个生命周期内都不会被使用。
持有并等待的线程主动释放资源: 如果一个线程已经持有了一些资源,但在尝试获取更多资源时失败了,它可以选择主动释放自己已经持有的资源,并在稍后重试。
然而,这种方法可能导致活锁问题,即线程不断地放弃和重试,导致系统整体性能下降。 为了避免活锁,可以在重试时引入随机延迟。
允许锁抢占:即允许一个线程在持有某个资源的同时,被另一个线程抢占该资源。这种方式需要谨慎实现,因为它可能导致数据不一致或系统不稳定。
b. 避免死锁
银行家算法:这是一种经典的避免死锁算法,通过模拟资源分配过程来预测是否会发生死锁,从而避免死锁的发生。然而,银行家算法在实际应用中较为复杂,且需要系统状态的完整信息,因此并不总是可行的。
资源分配图算法:通过构建资源分配图来监测系统的资源分配情况,当检测到可能形成环路等待时,采取预防措施来避免死锁。这种方法同样需要系统状态的完整信息,并且可能增加系统的开销。
c. 死锁检测
通过定期检测系统的状态来确定是否发生了死锁。一旦检测到死锁,系统可以采取恢复措施。
超时法: 为每个线程设置一个超时时间,如果线程在超时时间内未能获得所需的全部资源,则认为发生了死锁。
资源分配图检测法: 利用资源分配图来检测是否形成了环路等待。
d.死锁恢复 终止进程: 选择一个或多个陷入死锁的线程,终止它们的执行。 这可能会导致数据丢失或服务中断,因此需要谨慎选择被终止的线程。 回滚:
将系统恢复到某个安全的状态点,通常是通过回滚事务来实现的。 这种方法可以保持数据的一致性,但可能需要额外的开销来维护事务的回滚点。 资源抢占:
从陷入死锁的线程中抢占一些资源,并分配给其他线程。 这种方法需要确保不会破坏系统的稳定性和数据的一致性。
e. 其他技术
使用乐观锁:乐观锁通常基于数据版本记录机制实现,通过比较数据版本号来避免冲突。这种方法适用于并发量较高且冲突较少的场景。
使用悲观锁:悲观锁大多数情况下依靠数据库的锁机制实现,以确保操作最大程度的独占性。但可能会带来较大的性能开销。
无锁数据结构:通过原子操作来实现无锁数据结构,从而避免互斥锁所带来的开销和复杂性。这种方法适用于一些特定的场景,如并发队列、哈希表等。
欢迎在评论区留言表达看法 或 提出你想学习的技术内容 与 面试问题,阿沛会将其作为下期更新的内容。
如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~

** -- 往期精彩回顾 – **