编程与 AI14 分钟阅读更新于 2026-08-04

线程同步为什么需要锁:互斥、条件变量与死锁

围绕互斥锁、读写锁、条件变量和死锁,解释多个执行流程如何安全访问共享数据,以及等待为什么可能变成相互卡住。

相关工具

并发执行为什么会把简单操作变复杂

多个线程同时推进时,问题往往不在于某一条语句写错,而在于它们交错的时间顺序。看起来简单的“读取一个数字、加一、写回去”,实际包含多个步骤。一个线程刚读出旧值,另一个线程可能已经读走同一个旧值并写回结果,前一个线程再写回时就会覆盖更新。

共享数据的问题不能靠观察几次运行结果来判断是否存在。调度时机、处理器数量和系统负载都会改变线程交错的顺序,同一段程序可能大多数时候正常,偶尔才出现错误。同步机制的作用,就是把必须连续完成的操作明确圈起来,让并发执行仍然遵守可预测的规则。

两个线程交错读写共享数据导致更新丢失的示意图
共享数据的竞争

两个线程同时读取和写回同一数据,交错顺序可能让一次更新被覆盖。

互斥锁保护临界区

互斥锁只有两种基本状态:已加锁和已解锁。线程准备访问共享资源前先尝试加锁,成功后进入临界区;访问完成后释放锁。其他线程如果此时也要进入同一临界区,就会等待,直到持有锁的线程释放它。这样,同一时刻只有一个线程能执行受保护的那段操作。

锁保护的范围需要足够覆盖共享数据的一次完整更新,但也不宜把大量无关工作放进去。锁持有时间过长,会让其他线程长时间等待;锁覆盖范围过小,又可能把本应连续的动作拆开,留下竞争窗口。好的同步边界通常对应一个清楚的共享状态变化。

互斥锁保护临界区并让其他线程等待的流程图
互斥锁与临界区

线程进入共享数据区域前加锁,离开后解锁,其他线程在锁被占用时等待。

读写锁适合读多写少的场景

如果共享数据经常被读取,却很少被修改,让所有读取者互相排队会浪费并发能力。读写锁把访问分成读锁和写锁:多个线程可以同时持有读锁,但写线程需要独占,不能和其他读者或写者并存。只有确认没有写者占用或等待冲突时,读者才可以进入。

读写锁的价值来自访问模式,而不是“比互斥锁更高级”。如果写操作很多,读写锁的管理成本可能抵消并发收益;如果读取持续时间很长,还要考虑写者是否会长期等待。选择同步工具前,应该先弄清共享数据的读写比例和一致性要求。

条件变量解决的是等待条件

互斥锁解决“同一时刻谁能访问”,却不能单独表达“数据什么时候准备好”。例如消费者拿到锁后发现队列为空,它不应该一直占着锁循环检查,否则生产者也无法拿到锁添加任务。条件变量提供了另一种协作方式:条件不满足时,线程暂时等待并释放相关锁;条件满足后,生产者通知等待者重新尝试。

被唤醒不代表条件一定已经满足。可能有多个线程同时等待,只有一个线程能先拿到锁并取走数据,也可能在等待期间状态再次发生变化。因此线程被唤醒后仍要重新检查条件。条件变量负责通知状态可能变化,真正的状态判断仍然要由共享数据和保护它的锁共同完成。

生产者和消费者通过互斥锁与条件变量协作的流程图
条件变量的等待与唤醒

消费者发现条件不满足后释放锁并等待,生产者改变状态后发出通知,消费者醒来再重新检查。

死锁是相互等待形成的闭环

如果一组线程或进程中的每一个都在等待只能由其他成员触发的事件,就形成了死锁。最常见的例子是:线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1。两边都不愿意先释放已经拿到的锁,于是任务没有任何一方能够继续。

死锁通常和四个条件有关:资源互斥使用,线程已经占有资源又继续等待,已分配资源不能被强行夺走,以及等待关系形成环路。只有这些条件同时存在,死锁才有机会发生。分析问题时,可以把线程和资源画成有向关系图,沿着等待边寻找闭环。

两个线程互相持有并等待锁资源形成死锁的关系图
死锁中的循环等待

线程 A 等待线程 B 持有的资源,线程 B 又等待线程 A 持有的资源,形成无法自行打破的环路。

死锁的处理思路

一种思路是忽略死锁,把资源和实现复杂度留在可接受范围内,适用于发生概率很低且影响有限的场景。另一种思路是检测和恢复:定期寻找等待环路,发现后通过释放、回滚或终止部分任务打破循环。还有一种思路是预防,通过改变资源申请顺序或破坏死锁的必要条件,降低形成闭环的可能。

资源分配前还可以检查系统是否仍处于安全状态。所谓安全,不是保证任务马上完成,而是存在某种安排,使所有进程都能在未来逐步获得所需资源并结束。如果某次分配会让系统进入无法找到完成顺序的状态,就应该暂缓或拒绝这次请求。

如何减少同步带来的新问题

同步工具本身也会产生开销和风险。锁太多会增加理解难度,锁的顺序不一致会制造死锁,临界区过大又会降低并发度。比较稳妥的做法是先识别真正共享的数据,再为每类共享状态确定清楚的保护边界,尽量让加锁和解锁成对出现。

涉及多个锁时,可以为资源建立统一的获取顺序,所有线程都按同一顺序申请,避免 A 等 B、B 等 A 的闭环。涉及等待条件时,先明确条件由谁改变、谁负责通知,以及等待结束后如何重新检查。同步的重点不是把所有地方都锁住,而是让状态变化有可追踪的规则。

用一条主线理解线程同步

互斥锁解决共享资源的同时访问,读写锁区分并发读取和独占写入,条件变量解决线程在条件未满足时的等待与唤醒,死锁则提醒我们:多个资源和等待关系组合起来后,系统可能失去继续推进的路径。它们都围绕同一个问题展开:并发任务如何在共享状态上保持一致。

学习同步机制时,可以按“共享什么、谁能访问、什么时候可以访问、访问完成后如何通知”这条线观察。再检查是否存在多个锁、资源申请顺序是否一致、等待是否形成环路,就能从现象回到同步设计本身。

常见问题

加了互斥锁就不会有并发问题了吗?

不一定。锁必须覆盖正确的共享状态和完整的更新过程,锁的顺序、释放时机和异常路径也需要一致,否则仍可能出现竞争、长期等待或死锁。

条件变量是不是一种锁?

不是。条件变量用于等待和通知,通常需要和互斥锁一起使用;互斥锁保护共享状态,条件变量表示状态可能发生了变化。

死锁和普通阻塞有什么区别?

普通阻塞通常会在某个外部事件发生后继续,死锁则是多个任务互相等待形成闭环,等待条件无法由系统中的任何一方自行打破。