为什么你的代码会死锁?这四个条件一旦满足,必挂!
最近和朋友撸串,聊到Java多线程的各种坑,果然大家都是“被死锁支配过的灵魂”。有哥们一脸郁闷地说:“我的代码怎么老莫名其妙死锁啊?”我一听,哈哈,这可是我踩过的命里大劫。今天就唠唠这话题——死锁,是怎么死得那么死的。
死锁的四大天王
先说点人话,死锁是啥?就是A线程等B资源,B线程等A资源,大家互相杵着——谁都别想动!
教科书里经典说法:必须满足下面四个条件,死锁才会出现。为了方便记忆,我都给取了个外号:
| 条件 | 外号 | 解释 |
|---|---|---|
| 互斥 | “只许一人” | 每次只有一个线程能用资源 |
| 不可抢占 | “老子就不让” | 别人用着的资源,谁都不许抢 |
| 占有且等待 | “我不松手” | 先拿着一部分,然后傻傻地还等着别的资源 |
| 循环等待 | “连环等人” | 一个等一个,最后又绕回来等自己(就像汉服群的八卦……) |
哎呀, 写着写着就死锁了……
其实刚开始我觉得这种事只会发生在小白身上,直到有天在项目里搞两个锁,结果就在测试环境里挂了大半天。
代码可能长这样(伪代码,别当真小抄——凑个气氛就行):
synchronized(lockA) {
// ...
synchronized(lockB) {
// do something
}
}
// 另一处
synchronized(lockB) {
// ...
synchronized(lockA) {
// do something else
}
}
乍一看都没毛病。可惜,我A线程拿A锁等B锁,B线程拿B锁等A锁,两边互不相让。然后你就发现服务像咸鱼一样躺住了,怎么踹都不起来。
踩坑瞬间
有几个经典的“我为死锁狂”时刻,肯定不少人都有感(别谦虚,快认领):
- 杜撰了点所谓“优雅并发”,结果两个线程互锁住,debug半天发现卡死在同步块里
- 上生产的时候,一到高并发就疯狂死锁,但本地单步压根儿没复现
- 以为有了tryLock就安全了,结果忘记tryLock后没拿到锁还傻等
- 用 synchronized 嵌套太随意,锁顺序乱成锅粥
解决死锁的“土味技巧”
话说到底,如何防死锁?
- 统一锁顺序
咋都要锁A和B,就别一个A->B一个B->A。全公司写代码的统一风格,多了少了都不好说。 - 能用tryLock就用tryLock
不要强上,拿不到锁就及时放弃,别一死嗑到底。 - 避免长同步块
同步块里写少点,别“买锁就包场”,干完赶紧释放资源。 - 能不用锁就不用锁
乐观锁、并发容器啥的用起来,不要啥都synchronized。
经验启示
有几点“心头警钟”,写在这里,后面犯错就回来对照一下:
- 死锁不是魔法,是你亲手造的。四大条件,一条都不少就会出事。
- 本地OK不等于线上OK,多线程世界充满幻想和意外。
- 写多线程得像谈恋爱——别贪,别抢,别拧巴。
- 日志、监控、jstack,大招永远要在手。
说白了,多线程里的死锁,就是“等一个永远也等不到的人”。别再让你的小伙伴互相等死,写代码嘛,咱图个舒心!
——今天就唠到这,耳边又响起leader的声音:“下次写并发,给我规范点!” 收键盘,溜了溜了……
随时随地看视频