死锁到底是什么?
先讲个生活场景。
你和朋友开车去停车场,两辆车挤在一个窄巷子里。你的车头堵住出口,他的车头也堵住了。你想出去得让他先挪,他想出去你得先挪。结果俩车互相卡着,谁也动不了。
这就是 死锁(Deadlock)。在编程里,两个或多个线程各占着一把锁,都在等对方手里的锁,结果大家谁都拿不到,程序就这么卡死不动了。
死锁一旦触发,通常不会自己解开,程序要么一直挂着,要么你把进程干掉。线上服务要是中了这个招,那就是直接宕机。
死锁要同时满足 4 个条件
死锁不是随便来的,必须 4 个条件全齐了才会发生:
| 条件 | 意思 |
|---|---|
| 互斥 | 一把锁同一时刻只给一个线程用 |
| 占有并等待 | 线程拿着自己的锁,还去等别的锁 |
| 不可剥夺 | 锁只能自己主动放,别人抢不走 |
| 循环等待 | A等B、B等A,形成闭环 |
缺任何一个,死锁就发不了。你拆掉"循环等待"这一环,死锁立刻解除。所以所有解法,本质都是在破坏这四个条件里的某一个。
4 种解法,各拆一个条件
1. 固定加锁顺序(拆循环等待)
让所有线程都按同一个顺序拿锁。比如都先拿 A 锁再拿 B 锁。谁都不许反过来,闭环就断了。这是最常用、最推荐的做法,简单又可靠。
2. 设超时,拿不到就放手(拆不可剥夺)
拿锁的时候加个超时时间。比如 5 秒还拿不到,就先把已经拿到的锁也放下,重新排队。线程能自己放弃资源,循环就断了。
3. 一次把所有锁拿齐(拆占有并等待)
别拿着一个锁去要另一个。要么一次性申请全部需要的锁,要么全放下。这样就不会"占着不放"。
4. 减小锁粒度(缓解互斥)
锁的范围越大,越容易和别人撞车。把大锁拆成小锁,每个线程只管自己那一小段,竞争的面变小了,死锁概率自然下降。
实际开发里怎么做
写多段代码时记住三件事:
第一,锁的顺序团队内要统一。哪怕只是约定俗成,也远好过各写各的。
第二,能用无锁结构就别硬上锁。比如用 ConcurrentHashMap、原子变量、通道这些,从根上绕开死锁。
第三,上线前用压测或死锁检测工具扫一遍。JVM 有 jstack 能抓线程栈,Linux 有 ftrace,很多语言也有内置的死锁检测器。别等线上出事才发现。
死锁不是高深的概念,核心就是"互相等、谁也动不了"。理解了 4 个条件,解法自然水到渠成。
下次代码莫名其妙卡住了,先别慌,看看是不是两个线程各占一把锁在互相等着——十有八九就是它。