目录
[1、破坏互斥条件(Mutual Exclusion)](#1、破坏互斥条件(Mutual Exclusion))
[2、破坏请求与保持条件(Hold and Wait)](#2、破坏请求与保持条件(Hold and Wait))
[3、破坏不可剥夺条件(No Preemption)](#3、破坏不可剥夺条件(No Preemption))
[4、破坏循环等待条件(Circular Wait)](#4、破坏循环等待条件(Circular Wait))
[破坏循环等待条件(Circular Wait)](#破坏循环等待条件(Circular Wait))
[2、银行家算法(Banker's Algorithm)](#2、银行家算法(Banker's Algorithm))
[1、悲观锁(Pessimistic Locking)](#1、悲观锁(Pessimistic Locking))
[2、乐观锁(Optimistic Locking)](#2、乐观锁(Optimistic Locking))
[4、读写锁(Read-Write Lock)](#4、读写锁(Read-Write Lock))
一、死锁定义与成因
死锁是指在一组并发执行的进程中,每个进程都持有某些资源且等待获取其他进程所持有的资源,从而导致所有相关进程都无法继续执行的一种永久阻塞状态。这种状态具有不可自行解除的特性,需要外部干预才能恢复系统正常运行。
典型场景示例
假设存在两个线程(线程A和线程B)需要协同操作两个共享资源(锁1和锁2):
- 
线程A成功获取锁1后尝试获取锁2
 - 
同时线程B成功获取锁2后尝试获取锁1

 - 
此时线程A等待线程B释放锁2,线程B等待线程A释放锁1
 - 
双方形成相互等待的循环,导致系统资源被永久占用

 
这种场景凸显了原子操作在复合操作中的局限性:虽然单个锁的获取是原子操作,但多个锁的组合获取不具备原子性,从而为死锁创造了条件。

二、死锁产生的四个必要条件
**死锁的预防和避免策略主要围绕破坏其四个必要条件展开。由于这四个条件必须同时满足才会发生死锁,因此只要破坏其中任意一个或多个条件,就能有效预防死锁。**以下是对每个条件的详细分析及对应的破坏方法:
1、破坏互斥条件(Mutual Exclusion)
定义:资源在任意时刻只能被一个进程独占使用,其他进程必须等待该资源释放后才能访问。
破坏方法:
- 
允许资源共享:将独占资源改为共享资源(如读操作共享文件锁)。
- 
适用场景:适用于读多写少的场景(如数据库的读锁)。
 - 
局限性:并非所有资源都支持共享(如打印机、写操作等必须互斥)。
 
 - 
 - 
虚拟化资源:通过技术手段将独占资源虚拟化为多个可共享的逻辑资源。
- 示例:使用虚拟打印机(每个进程分配独立的虚拟打印队列)。
 
 
效果:
- 
完全消除互斥条件会降低系统对资源的控制能力,可能引发数据不一致等问题。
 - 
通常不作为主要策略,而是结合其他条件破坏方法使用。
 
2、破坏请求与保持条件(Hold and Wait)
定义:进程在持有至少一个资源的同时,请求新的资源并被阻塞等待。
破坏方法:
- 
一次性申请所有资源:
- 
进程在开始执行前,必须一次性申请所有需要的资源。
 - 
若系统无法满足全部请求,则释放已持有的资源并等待。
 - 
示例:银行家算法中,进程需提前声明最大资源需求。
 - 
优点:简单直接,彻底消除请求与保持。
 - 
缺点:
- 
可能导致资源利用率低(进程因部分资源不足而长期阻塞)。
 - 
适用于资源需求可预知的场景(如批处理系统)。
 
 - 
 
 - 
 - 
资源预分配策略:
- 
进程在启动时分配所有必要资源,运行期间不再申请新资源。
 - 
适用场景:资源需求稳定的长期任务(如科学计算)。
 
 - 
 
效果:
- 
显著减少死锁概率,但可能降低系统并发性能。
 - 
需权衡资源利用率与死锁风险。
 

3、破坏不可剥夺条件(No Preemption)
定义:已分配给进程的资源,在该进程未主动释放前,不能被其他进程强行夺取。
破坏方法:
- 
资源抢占(Preemption):
- 
当进程请求新资源被拒绝时,强制释放其已持有的部分或全部资源。
 - 
被抢占的进程进入等待状态,稍后重试。
 - 
关键点:
- 
需确保被抢占的资源状态可恢复(如通过回滚操作)。
 - 
可能引发进程饥饿(某些进程反复被抢占)。
 
 - 
 - 
示例:
- 
操作系统强制终止长时间未响应的进程。
 - 
数据库系统中,事务因死锁被回滚。
 
 - 
 
 - 
 - 
超时机制:
- 
为资源请求设置超时时间,超时后自动释放已持有资源。
 - 
适用场景:实时系统或对响应时间敏感的场景。
 
 - 
 
效果:
- 
增加系统复杂性(需处理资源回滚和状态恢复)。
 - 
适用于资源可抢占的场景(如CPU、内存),但不适用于所有资源(如打印机)。
 

4、破坏循环等待条件(Circular Wait)
定义:存在一个进程的循环链,每个进程都在等待下一个进程所占用的资源。
破坏方法:
- 
资源有序分配法(Hierarchical Allocation):
- 
为所有资源类型定义全局编号(如锁1、锁2、锁3)。
 - 
要求进程必须按编号顺序请求资源(如先申请锁1,再申请锁2)。
 - 
原理:通过强制顺序请求,消除交叉等待形成的环路。
 - 
示例:
cpp// 错误示例(可能导致死锁): // 线程A: lock(mtx1); lock(mtx2); // 线程B: lock(mtx2); lock(mtx1); // 正确示例(固定顺序): // 所有线程必须先锁mtx1,再锁mtx2 - 
优点:实现简单,无需复杂检测机制。
 - 
缺点:
- 
需预先定义资源顺序,可能不灵活。
 - 
可能导致资源利用率不均衡(某些资源被过度请求)。
 
 - 
 
 - 
 - 
超时重试:
- 
结合超时机制,当进程等待资源超时后,释放已持有资源并重新尝试。
 - 
效果:通过随机性打破固定等待顺序。
 
 - 
 
效果:
- 
是实际应用中最常用的死锁预防策略之一。
 - 
需结合具体场景设计资源顺序,避免人为引入新的循环等待。
 

综合对比与选择策略
| 条件 | 破坏方法 | 优点 | 缺点 | 适用场景 | 
|---|---|---|---|---|
| 互斥 | 资源共享、虚拟化 | 简单直接 | 降低资源控制能力 | 读多写少场景 | 
| 请求与保持 | 一次性申请、预分配 | 彻底消除死锁风险 | 资源利用率低 | 批处理、长期任务 | 
| 不可剥夺 | 资源抢占、超时 | 灵活性强 | 实现复杂,可能引发饥饿 | 实时系统、可回滚资源 | 
| 循环等待 | 资源有序分配、超时重试 | 实现简单,效果显著 | 需预定义顺序,可能不灵活 | 通用并发场景 | 
实际应用建议
- 
优先破坏循环等待:通过固定资源申请顺序(如锁的层级)是最简单有效的方法。
 - 
结合超时机制:为资源请求设置超时,避免长时间阻塞。
 - 
避免过度预防:根据场景选择合适策略,例如:
- 
高并发服务:重点破坏循环等待(如使用读写锁)。
 - 
实时系统:结合资源抢占和超时。
 - 
批处理系统:采用一次性申请策略。
 
 - 
 
通过合理组合这些方法,可以构建高效且健壮的并发系统。
三、死锁预防与避免策略
1、破坏循环等待条件
- 
资源一次性分配策略:要求进程一次性申请所有需要的资源,若无法满足则不分配任何资源。这种策略通过消除部分获取的可能性来打破循环等待。
 - 
超时重试机制:为资源请求设置时间阈值,超时后释放已持有资源并重试。这可以有效打破长时间的等待循环。
 - 
加锁顺序一致性:强制所有线程按照固定顺序获取锁资源。例如规定总是先获取锁1再获取锁2,可以消除不同顺序导致的交叉等待。
 
2、代码实现示例(C++)
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <mutex>
#include <thread>
#include <vector>
// 共享资源定义
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;
// 安全访问共享资源的函数
void access_shared_resources() {
    // 采用固定顺序获取锁
    std::lock_guard<std::mutex> lock1(mtx1);  // 先获取mtx1
    std::lock_guard<std::mutex> lock2(mtx2);  // 再获取mtx2
    
    // 安全访问共享资源
    for (int i = 0; i < 10000; ++i) {
        ++shared_resource1;
        ++shared_resource2;
    }
}
// 模拟并发访问
void simulate_concurrent_access() {
    std::vector<std::thread> threads;
    
    // 创建10个并发线程
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(access_shared_resources);
    }
    
    // 等待所有线程完成
    for (auto &thread : threads) {
        thread.join();
    }
    
    // 输出结果验证
    std::cout << "Final State - Resource1: " << shared_resource1 
              << ", Resource2: " << shared_resource2 << std::endl;
}
int main() {
    simulate_concurrent_access();
    return 0;
}
        输出结果

这段代码通过固定锁的获取顺序 (先 mtx1 后 mtx2)来避免死锁,直接体现了**破坏循环等待条件(Circular Wait)**这一死锁预防策略。
代码与死锁的关系
潜在死锁场景
如果两个线程以不同的顺序请求锁,例如:
- 
线程A :先锁
mtx1,再锁mtx2。 - 
线程B :先锁
mtx2,再锁mtx1。 
当以下事件顺序发生时,会触发死锁:
- 
线程A获取
mtx1,线程B获取mtx2。 - 
线程A尝试获取
mtx2(被线程B持有,阻塞)。 - 
线程B尝试获取
mtx1(被线程A持有,阻塞)。 - 
两个线程互相等待,形成循环等待条件,导致死锁。
 
本代码的改进
通过强制所有线程按相同顺序获取锁 (先 mtx1 后 mtx2),破坏了循环等待条件:
- 
即使多个线程并发执行,也不会出现交叉请求锁的情况。
 - 
线程B无法在持有
mtx2的同时等待mtx1,因为线程A已经按顺序完成了操作。 
代码体现的死锁预防策略
破坏循环等待条件(Circular Wait)
- 
方法:定义全局资源(锁)的申请顺序,要求所有线程严格遵守。
 - 
代码实现:
cppstd::lock_guard<std::mutex> lock1(mtx1); // 固定先锁mtx1 std::lock_guard<std::mutex> lock2(mtx2); // 再锁mtx2 - 
效果:
- 
消除了循环等待的可能性。
 - 
即使并发线程数量增加(如代码中的10个线程),也不会因锁顺序问题导致死锁。
 
 - 
 
对比其他策略
- 
互斥条件:未被破坏(锁本身仍是互斥的)。
 - 
请求与保持:未被破坏(线程在持有锁时仍可能阻塞等待其他资源,但本例中无其他资源请求)。
 - 
不可剥夺:未被破坏(锁的释放仍是主动的,未实现抢占)。
 
3、避免锁未释放的实践
- 
使用RAII(资源获取即初始化)模式的锁管理,如
std::lock_guard或std::unique_lock - 
确保所有代码路径(包括异常情况)都能正确释放锁
 - 
避免在持有锁时执行可能阻塞的操作(如I/O操作)
 - 
限制锁的持有时间,保持临界区代码简洁
 
四、死锁处理算法(进阶知识)
1、死锁检测算法
通过构建资源分配图并检测其中的环路来判断是否存在死锁。主要步骤包括:
- 
构建进程-资源有向图
 - 
使用深度优先搜索检测环路
 - 
若发现环路则确认死锁存在
 
2、银行家算法(Banker's Algorithm)
一种经典的死锁避免算法,通过资源分配的安全序列检查来预防死锁:
- 
维护系统可用资源向量
 - 
跟踪各进程的最大需求和已分配资源
 - 
在分配资源前检查系统是否处于安全状态
 - 
仅当存在安全序列时才进行资源分配
 
3、资源有序分配法
- 为所有资源类型定义全局编号,要求进程必须按编号顺序请求资源。这种方法通过消除循环等待的可能性来预防死锁。
 
五、最佳实践建议
- 
设计阶段预防:在系统设计初期就考虑死锁可能性,采用层次化的资源分配策略
 - 
最小化锁粒度:将大锁拆分为多个细粒度锁,减少竞争范围
 - 
读写锁优化:对读多写少的场景使用读写锁替代互斥锁
 - 
超时机制:为所有锁操作设置合理的超时时间
 - 
监控与告警:实现死锁检测机制,在生产环境中实时监控
 - 
压力测试:通过模拟高并发场景验证死锁处理机制的有效性
 
通过系统性的死锁预防策略和严谨的编码实践,可以显著降低死锁发生的概率,构建更加健壮的并发系统。
六、补充:其他常见锁类型(简要概述)
1、悲观锁(Pessimistic Locking)
在每次读取数据时,为了防止其他线程修改数据,通常会先获取相应的锁(如读锁、写锁或行锁)。这样当其他线程尝试访问该数据时,就会被阻塞并挂起。
- 
核心思想:假设并发冲突频繁发生,操作前先加锁。
 - 
常见实现:
- 
互斥锁(Mutex) :如
std::mutex,阻塞其他线程直到锁释放。 - 
读写锁(Read-Write Lock) :允许多个读线程或一个写线程(如
std::shared_mutex)。 - 
数据库行锁:事务中锁定特定行。
 
 - 
 - 
适用场景:高竞争环境(如银行账户操作)。
 
2、乐观锁(Optimistic Locking)
在读取数据时,通常采用乐观锁机制,即假设数据在读取期间不会被其他线程修改,因此不需要加锁。但在更新数据前,会先校验数据是否已被修改,主要通过两种方式实现:版本号机制和CAS操作。
- 
核心思想:假设冲突较少,操作前不加锁,提交时检查冲突。
 - 
常见实现:
- 
版本号机制:数据附带版本号,更新时校验版本是否变更。
 - 
CAS(Compare-And-Swap) :原子操作,比较内存值与预期值,相等则更新。在执行数据更新时,系统会先比较内存中的当前值与之前获取的值是否一致。若两者相同,则执行新值更新操作;若不一致,操作将失败并触发重试机制,通常表现为持续的自旋重试过程。
cppstd::atomic<int> value(0); int expected = 0; value.compare_exchange_strong(expected, 1); // CAS操作 
 - 
 - 
适用场景:读多写少(如无锁队列、并发计数器)。
 
3、自旋锁(Spinlock)
- 
特点:线程忙等待(循环检查锁状态)而非阻塞,避免上下文切换开销。
 - 
问题:长时间等待会浪费CPU资源。
 - 
适用场景:锁持有时间极短(如内核同步)。
 
4、读写锁(Read-Write Lock)
- 
特点:区分读/写操作,允许多个读线程或一个写线程。
 - 
C++17实现 :
std::shared_mutex+std::shared_lock(读锁)/std::unique_lock(写锁)。 
5、分布式锁
- 场景:跨进程/机器的同步(如Redis实现的分布式锁)。
 
总结
- 
STL容器:默认非线程安全,需用户通过同步机制保护。
 - 
智能指针:
- 
unique_ptr:通常不涉及线程安全。 - 
shared_ptr:引用计数原子操作安全,但对象访问需同步。 
 - 
 - 
锁策略:
- 
悲观锁适合高竞争,乐观锁适合低竞争。
 - 
CAS是乐观锁的核心原子操作。
 - 
自旋锁和读写锁针对特定场景优化。
 
 - 
 
如需深入锁的实现细节(如条件变量、RAII封装),可参考后续学习博客更新或《C++ Concurrency in Action》等专业书籍。