面试八股文-死锁
死锁的四个必要条件("Coffman条件")
1.互斥条件:至少有一个资源是非共享的,必须由一个线程独占。
2.占有并等待:一个线程已经持有了一个资源,但还在请求其他资源并等待,而这些资源已被其他线程持有。
3.不可剥夺:资源不能被强行从线程中剥夺,必须由线程主动释放。
4.环形等待:存在一个环形链,即线程A等待线程B持有的资源,线程B又等待线程C持有的资源,依此类推,最后一个线程又等待线程A的资源,形成环路。
避免死锁的方案
为了避免死锁,可以从打破上述四个必要条件入手,主要有以下几种常用的方案:
- 破坏互斥条件
- 共享资源:尽可能避免将资源设置为互斥的。如果资源可以并发访问,则不会产生死锁。例如,读写锁(ReadWriteLock)可以允许多个线程同时读取数据而不互相阻塞。
- 破坏占有并等待条件
- 一次性申请所有资源:确保线程在进入临界区时一次性申请所需的所有资源,避免线程在获得部分资源后再去请求其他资源时发生死锁。例如,使用数据库连接时,提前申请好所有的锁和资源,而不是在过程中逐步申请。
- 非阻塞算法:使用非阻塞锁设计,例如采用乐观锁、CAS(Compare And Swap)等,避免在等待时阻塞。
- 破坏不可剥夺条件
- 可剥夺锁:如果一个线程已经占有了某些资源,但又无法得到其他资源时,可以暂时释放当前占有的资源,稍后重新尝试。这样可以避免资源长时间被占有,减小死锁的可能性。比如在数据库事务处理中,遇到锁冲突时回滚并重试。
- 破坏环形等待条件
- 资源顺序编号法:将所有的资源按照某个顺序编号,要求所有线程必须按照编号顺序请求资源,保证不会形成环路。例如,要求所有线程首先请求编号最小的资源,接着是第二小的资源,依此类推。这可以有效防止环形等待的产生。
- 超时机制:给每个线程设置一个超时时间,如果线程在一定时间内无法获取所需资源,就主动放弃并回滚操作,从而避免长时间等待导致的死锁。
常见的死锁解决方案
- 死锁预防
- 死锁预防就是通过编程或者系统设计确保某些死锁条件不成立。
- 避免同时持有多个资源:尽可能将任务分解,使得每个线程只需要持有一个资源。
- 提高并发粒度:将锁的粒度调细,避免多个线程频繁竞争同一个资源。
- 避免循环等待:通过资源分配顺序确保不会发生循环等待。
- 死锁检测与恢复
- 死锁检测算法:通过系统检测当前的资源占用状态,判断是否存在死锁。如果检测到死锁,可以选择强制终止某个线程以释放资源。例如,银行家算法(Banker’s Algorithm)是一种经典的死锁检测与避免的算法。
- 资源回收:一旦检测到死锁,系统可以选择回收某些线程占有的资源,强制它们放弃操作,以恢复系统的正常运行。
- 避免死锁的工具和库
- 并发工具库:许多高级编程语言提供了处理并发和锁机制的库。例如,Java 的 java.util.concurrent.locks 包提供了 ReentrantLock、ReadWriteLock 等工具,有效帮助开发者避免死锁。
- 死锁检测工具:可以使用工具对系统进行分析和监控,检测可能发生的死锁。比如 Java 的 JConsole 或 JVisualVM 可以用于检测 JVM 中的线程死锁情况。