当前位置:首页>java>常见的死锁场景,你的代码中了几个?

常见的死锁场景,你的代码中了几个?

  • 2026-10-21 13:29:06
常见的死锁场景,你的代码中了几个?

嘿,小伙伴们,我是小康 👋

说实话,死锁是多线程编程中最让人头疼的Bug之一。它不像段错误那样会立即崩溃,而是悄无声息地让程序"卡死",CPU不高、内存不涨,就是不动了...

今天,我就来盘点一下实战中最常见的7种死锁场景,看看你的代码中了几个?

场景1:经典的锁顺序不一致

这是最经典、也是最常见的死锁场景。

💀 问题代码

std::mutex mtx1, mtx2;voidthread1_func(){std::lock_guard<std::mutex> lock1(mtx1);  // 先锁mtx1std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock2(mtx2);  // 再锁mtx2// 业务逻辑...}voidthread2_func(){std::lock_guard<std::mutex> lock2(mtx2);  // 先锁mtx2std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock1(mtx1);  // 再锁mtx1// 业务逻辑...}

死锁原因:线程1持有mtx1等待mtx2,线程2持有mtx2等待mtx1,形成循环等待。

✅ 正确解决方案

方案1:统一加锁顺序

voidsafe_thread_func(){// 所有线程都按照 mtx1 -> mtx2 的顺序加锁std::lock_guard<std::mutex> lock1(mtx1);std::lock_guard<std::mutex> lock2(mtx2);// 业务逻辑...}

方案2:使用 std::lock 原子获取多锁(推荐)

voidsafe_thread_func(){std::lock(mtx1, mtx2);  // 原子地同时获取两个锁std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);// 业务逻辑...}

方案3:使用 std::scoped_lock(C++17,最推荐)

voidsafe_thread_func(){std::scoped_lock lock(mtx1, mtx2);  // 自动按顺序加锁,RAII管理// 业务逻辑...}

场景2:忘记解锁导致的死锁

这个错误看似低级,但在实际项目中出现频率相当高!

💀 问题代码

std::mutex mtx;voidprocess_data(){    mtx.lock();if (some_error_condition) {return;  // 糟糕!忘记unlock就返回了    }// 业务逻辑...    mtx.unlock();}

死锁原因:第一个线程触发了提前返回,锁没有释放,后续所有线程都会永久阻塞。

✅ 正确解决方案

永远使用RAII管理锁,不要手动lock/unlock

voidsafe_process_data(){std::lock_guard<std::mutex> lock(mtx);  // 自动管理,异常安全if (some_error_condition) {return;  // lock_guard析构时会自动unlock    }// 业务逻辑...}  // 离开作用域自动unlock

场景3:递归锁的误用

同一个线程对同一个普通mutex多次加锁会死锁!

💀 问题代码

std::mutex mtx;voidfunc_a(){std::lock_guard<std::mutex> lock(mtx);// 业务逻辑...    func_b();  // 调用func_b}voidfunc_b(){std::lock_guard<std::mutex> lock(mtx);  // 再次锁同一个mutex,死锁!// 业务逻辑...}

死锁原因:std::mutex不支持递归加锁,同一线程第二次lock会阻塞自己。

✅ 正确解决方案

方案1:使用递归锁

std::recursive_mutex mtx;  // 改用递归锁voidfunc_a(){std::lock_guard<std::recursive_mutex> lock(mtx);    func_b();  // 可以再次加锁,不会死锁}voidfunc_b(){std::lock_guard<std::recursive_mutex> lock(mtx);// 业务逻辑...}

方案2:重构代码,避免递归锁(更推荐)

std::mutex mtx;voidfunc_a(){std::lock_guard<std::mutex> lock(mtx);// 业务逻辑...    func_b_internal();  // 调用不加锁的内部函数}voidfunc_b(){std::lock_guard<std::mutex> lock(mtx);    func_b_internal();}voidfunc_b_internal(){// 实际的业务逻辑,不加锁// 调用者保证已经持有锁}

场景4:条件变量的死锁陷阱

条件变量使用不当也会导致死锁,这个很多人不知道!

💀 问题代码

std::mutex mtx;std::condition_variable cv;bool ready = false;// 生产者voidproducer(){    ready = true;  // 没有在锁保护下修改共享变量!    cv.notify_one();}// 消费者voidconsumer(){std::unique_lock<std::mutex> lock(mtx);    cv.wait(lock, []{ return ready; });  // 可能永久等待!// 业务逻辑...}

死锁原因:如果notify_one()在wait()之前发生,通知会丢失,消费者永久等待。

✅ 正确解决方案

共享变量的修改必须在mutex保护下进行

std::mutex mtx;std::condition_variable cv;bool ready = false;// 生产者voidproducer(){    {std::lock_guard<std::mutex> lock(mtx);        ready = true;  // 在锁保护下修改    }    cv.notify_one();  // 可以在锁外通知(性能更好)}// 消费者voidconsumer(){std::unique_lock<std::mutex> lock(mtx);    cv.wait(lock, []{ return ready; });  // 即使通知先到,也能检测到ready为true// 业务逻辑...}

场景5:线程join引起的死锁

没有锁也能死锁?是的!thread::join()也可能导致死锁。

💀 问题代码

std::thread t1, t2;voidthread1_func(){// 业务逻辑...    t2.join();  // 等待t2结束}voidthread2_func(){// 业务逻辑...    t1.join();  // 等待t1结束}intmain(){    t1 = std::thread(thread1_func);    t2 = std::thread(thread2_func);// 两个线程互相等待,死锁!    t1.join();    t2.join();}

死锁原因:t1在等t2结束,t2在等t1结束,形成循环等待。

✅ 正确解决方案

不要让线程互相join,在主线程统一管理

std::thread t1, t2;voidthread1_func(){// 业务逻辑,不join其他线程}voidthread2_func(){// 业务逻辑,不join其他线程}intmain(){    t1 = std::thread(thread1_func);    t2 = std::thread(thread2_func);// 在主线程按顺序join    t1.join();    t2.join();}

场景6:持锁时间过长导致的隐性死锁

这种场景不是真正的死锁,但会让程序"看起来像死锁"。

💀 问题代码

std::mutex io_mutex;voidthread_func(){std::lock_guard<std::mutex> lock(io_mutex);// 持锁期间做耗时操作std::cout << "Processing..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(10));  // 模拟耗时操作// 其他线程会长时间阻塞}

问题:虽然不是死锁,但其他线程会长时间等待,影响性能。

✅ 正确解决方案

缩小临界区,减少持锁时间

std::mutex io_mutex;voidthread_func(){// 耗时操作放在锁外std::this_thread::sleep_for(std::chrono::seconds(10));// 只在必要时加锁    {std::lock_guard<std::mutex> lock(io_mutex);std::cout << "Processing..." << std::endl;    }  // 尽快释放锁}

场景7:对象交换时的死锁(高级场景)

这是一个非常隐蔽的死锁场景,经常出现在需要交换两个对象数据的情况下。

💀 问题代码

classAccount {mutablestd::mutex mtx;int balance;public:voidtransfer(Account& to, int amount){std::lock_guard<std::mutex> lock1(this->mtx);  // 锁自己std::lock_guard<std::mutex> lock2(to.mtx);     // 锁对方this->balance -= amount;        to.balance += amount;    }};// 使用Account acc1, acc2;std::thread t1([&]{ acc1.transfer(acc2, 100); });  // acc1 -> acc2std::thread t2([&]{ acc2.transfer(acc1, 50); });   // acc2 -> acc1,死锁!

死锁原因:t1锁定acc1再锁acc2,t2锁定acc2再锁acc1,顺序相反。

✅ 正确解决方案

使用唯一ID确定加锁顺序

classAccount {staticstd::atomic<unsignedint> next_id;constunsignedint id;mutablestd::mutex mtx;int balance;public:    Account() : id(next_id++), balance(0) {}voidtransfer(Account& to, int amount){if (this == &to) return;  // 防止自己给自己转账// 根据ID决定加锁顺序std::mutex* first = (this->id < to.id) ? &this->mtx : &to.mtx;std::mutex* second = (this->id < to.id) ? &to.mtx : &this->mtx;std::lock_guard<std::mutex> lock1(*first);std::lock_guard<std::mutex> lock2(*second);this->balance -= amount;        to.balance += amount;    }};std::atomic<unsignedint> Account::next_id{0};

更简洁的方案(C++17)

voidtransfer(Account& to, int amount){if (this == &to) return;// std::scoped_lock会自动按地址排序加锁std::scoped_lock lock(this->mtx, to.mtx);this->balance -= amount;    to.balance += amount;}

📝 避免死锁的黄金法则

总结一下,要避免死锁,记住这些原则:

  1. 使用RAII管理锁: 永远用lock_guard或scoped_lock,不要手动lock/unlock
  2. 统一加锁顺序: 多个锁时,保证全局加锁顺序一致
  3. 使用std::lock或std::scoped_lock:原子获取多个锁,避免顺序问题
  4. 避免嵌套锁:尽量一次只持有一个锁
  5. 缩小临界区:减少持锁时间,降低死锁概率
  6. 条件变量要正确使用:共享变量的修改必须在mutex保护下
  7. 不要循环等待:避免线程间互相join

🎯 想彻底解决死锁问题?

手动避免死锁虽然重要,但在复杂项目中,人工检查很容易遗漏。这时候就需要自动化的死锁检测工具!

我最近开发了一个DeadLock-Sentinel 死锁检测工具,可以:

  • ✅ 自动检测任意复杂的死锁场景
  • ✅ 精确定位到源代码行号
  • ✅ 显示完整的函数调用栈
  • ✅ 零侵入集成,只需改类型名

感兴趣的话,加我微信 jkfwdkf,备注「死锁检测」

或者扫描下方二维码:


如果这篇文章对你有帮助,记得点赞、在看、转发三连!🔥

你的项目中遇到过哪些死锁场景?欢迎在评论区分享你的踩坑经历! 👇

END

作者:xiaokang1998

来源:跟着小康学编程

版权归原作者所有,如有侵权,请联系删除。
▍推荐阅读
嵌入式状态机架构,一文讲透!
造福无数程序员的技术,被无情地抛弃了…
用了6年的米家吹风筒罢工了,寿命瓶颈原来在这里
→点关注,不迷路←

最新文章

随机文章