⚠️ 并发五大常见陷阱
目录
- 数据竞争 (Data Race)
- 死锁 (Deadlock)
- 竞态条件 & 饿死现象 (Race Condition & Starvation)
- 悬挂指针 (Dangling Pointer)
- 重复释放 (Double Free)
- 开发自查清单
1. 数据竞争 (Data Race)
专业定义
两个及以上线程在缺乏同步的情况下同时访问同一地址,并且至少有一个线程执行写操作。
通俗比喻
两个人同时在同一张 Excel 表里改同一个单元格,最后单元格的内容谁也说不准。
危害
状态错乱、随机崩溃、难以复现。常见场景
- 全局计数器
counter++
- 缓存读写(读线程没加锁)
- 全局计数器
极简示例(C11)
int counter = 0; void* add(void* _) { for (int i = 0; i < 1000000; ++i) counter++; return NULL; }
发现方法
ThreadSanitizer、Helgrind、Rust 编译器借用检查。避免策略
互斥锁Mutex
、原子类型Atomic*
、消息传递 (channel),“不共享即不竞争”。
2. 死锁 (Deadlock)
专业定义
多线程因为循环等待资源导致相互阻塞且永不释放。通俗比喻
A 把门钥匙握在手里等 B 的车钥匙,B 把车钥匙握在手里等 A 的门钥匙——谁也别想走。死锁四必要条件
互斥、占有并等待、不可抢占、循环等待。典型示例(Java)
synchronized(lockA) { synchronized(lockB) { /* … */ } } // 另一线程 synchronized(lockB) { synchronized(lockA) { /* … */ } }
发现方法
jstack
看线程栈、Rustcargo-deadlock
、Go 运行时死锁检测。避免策略
固定锁顺序、try_lock
+ 超时回退、细粒度锁、用 Channel / Actor 模式取代共享锁。
3. 竞态条件 & 饿死现象
3.1 竞态条件 (Race Condition)
专业定义
程序的正确性依赖于多个事件的执行顺序,而该顺序又未受控制。通俗比喻
检查房门没锁 → 去拿快递 → 回来发现小偷已进屋——检查‑后‑执行窗口被抢占。场景示例(Shell)
if [ ! -e /tmp/mydir ]; then mkdir /tmp/mydir # 另一个进程可能在检查后立即创建 fi
修复关键
将“检查 + 创建”做成一个原子操作(加锁,或用mkdir -p
让系统保证原子性)。
3.2 饿死现象 (Starvation)
定义
线程长期得不到 CPU 时间片或资源,处于“活着却干不了活”状态。典型场景
- 读写锁:大量读锁使写锁一直被饿死
- 严格优先级调度:低优先级线程永远排不到
缓解
公平锁、优先级继承、动态调度策略。
4. 悬挂指针 (Dangling Pointer)
专业定义
指针仍指向一块已释放或作用域结束的内存区域。通俗比喻
拿着老房子的钥匙,房子已被拆迁,再开门只会撞墙。示例(C)
int *p; { int x = 42; p = &x; } // x 生命周期结束 printf("%d\n", *p); // 悬挂访问
解决思路
RAII / 智能指针 / Rust 所有权模型自动禁止悬挂引用。
5. 重复释放 (Double Free)
专业定义
对同一内存块调用两次释放操作。通俗比喻
电影票撕过一次又被检票员再撕一次——第二次可能撕到别人的票。示例(C)
char *buf = malloc(100); free(buf); free(buf); // 第二次释放
危害
崩溃;更严重时可破坏堆结构,被黑客利用执行任意代码。防范
设置指针为NULL
、使用智能指针(unique_ptr
/ Rust 所有权自动回收)、开启 AddressSanitizer。
6. 开发生命线——五步自查
- 代码审查:重点看“检查‑后‑执行”与手动内存管理片段。
- 静态分析:开启编译器最大警告,IDE 并发检查。
- 动态探测:CI 跑 AddressSanitizer / ThreadSanitizer / Valgrind。
- 运行监控:锁等待时间、线程阻塞时长、队列深度。
- 语言特性先行:能用 RAII / 智能指针 / 所有权就别手写
malloc/free
。
一句话总结
“共享越少,风险越小;让编译器和工具兜底,用良好设计把错堵在出生之前。”