多线程的地狱级Bug:死锁如何产生?如何避免?

软件求生 2025-03-13 09:28:35



程序员小王的面试遭遇

“你先说说,什么是死锁?”

小王的社招面试刚刚进行到 Java 并发部分,面试官微微一笑,抛出了一个经典的问题。

小王心里一紧,暗道:“完了,这题我上周还复习过,怎么一上来就懵了?”

他深吸一口气,硬着头皮答道:“呃……死锁,就是……就是多个线程同时争夺多个共享资源,互相等待,导致程序卡住,谁也动不了。”

面试官点了点头,又问:“那你能说说,产生死锁的必要条件是什么吗?”

小王:“……”

什么是死锁?

如果你和小王一样,听到“死锁”就脑子一片空白,不妨来听个故事:

故事背景:程序员食堂

在程序员食堂,有两个程序员——小米和小李,他们坐在同一张桌子上吃饭,但桌子上只有一双筷子和一个勺子。

小米想吃饭,他拿起筷子,但他还需要勺子才能吃汤泡饭;小李想喝汤,他拿起勺子,但他还需要筷子才能夹菜。

现在问题来了——

小米已经拿了筷子,等着小李把勺子给他;

小李已经拿了勺子,等着小米把筷子给他。

两个人都不愿意放下自己手里的餐具,最终的结果就是两个人都吃不上饭,谁也不让谁,一直僵持。

这就是典型的死锁(Deadlock)!

在 Java 里,多个线程在持有锁的情况下,等待对方释放锁,就会导致死锁,程序就像被“卡住”了一样,不会报错,但也不会继续执行。

产生死锁的四个必要条件

那么,死锁为什么会发生呢?回到上面的故事,我们可以总结出死锁发生的四个条件(也称为“柯林斯四条件”):

1. 互斥条件(Mutual Exclusion)

资源一次只能被一个线程占用。

比如:一根筷子一次只能被一个人拿着,另一个人就没办法用。

2. 请求并保持条件(Hold and Wait)

一个线程持有资源 A,同时等待资源 B,而不释放已占用的资源 A。

比如:小米拿着筷子等勺子,小李拿着勺子等筷子,都不肯先放下。

3. 不可剥夺条件(No Preemption)

线程获取的资源不能被强行剥夺,只能由线程自己释放。

比如:你不能强行把筷子从小米手里抢走,也不能把勺子从小李手里抢走。

4. 循环等待条件(Circular Wait)

多个线程形成环状等待关系,比如:

线程 A 等 线程 B 的资源

线程 B 等 线程 C 的资源

线程 C 又等 线程 A 的资源

比如:小米等着小李放下勺子,小李等着小米放下筷子,两个人互相等待,形成了死循环。

只要这四个条件同时满足,死锁就会发生!

怎么在 Java 代码里制造死锁?

小王听完面试官的解释,恍然大悟:“那在 Java 里,我们怎么能写出一个死锁的代码呢?”

面试官微微一笑:“你可以这样试试——”

代码分析

thread1 先获取 lockA,然后尝试获取 lockB。

thread2 先获取 lockB,然后尝试获取 lockA。

两个线程都持有一个锁,并等待对方释放另一个锁,结果就是谁也等不到,形成死锁!

小王惊呆了:“太可怕了!那要怎么防止死锁呢?”

如何防止死锁?

死锁可怕,但并不是无解的,我们有四种主要方法来避免死锁:

1. 避免锁的嵌套(避免持有多个锁)

最简单的方法就是尽量不在一个线程里同时获取多个锁。

比如:让小米和小李都只用筷子或只用勺子,就不会死锁了。

2. 保持锁的顺序一致

如果必须用多个锁,确保获取锁的顺序是一致的,比如所有线程都先获取 lockA 再获取 lockB。

比如:规定每个人先拿筷子再拿勺子,这样就不会互相等待了。

3. 设置超时时间

使用 tryLock() 方法来获取锁,并设置超时时间,避免无限等待。

4. 使用更高级的锁机制(如 ReentrantLock)

ReentrantLock 提供了 tryLock() 方法,可以让线程在一定时间内尝试获取锁,避免死锁。

总结

面试结束后,小王长舒一口气,总结了今天的收获:

死锁是多个线程互相等待对方释放资源,导致程序卡死的现象。

死锁发生的四个必要条件:互斥、请求并保持、不可剥夺、循环等待。

通过避免锁嵌套、保持锁顺序、设置超时、使用 ReentrantLock可以有效防止死锁。

面试官满意地点点头:“不错,下一个问题……”

你还在怕死锁吗?用上这些技巧,让你的 Java 代码远离死锁!

END

如果你觉得有收获,别忘了点赞+关注,后续还有更多并发编程干货等着你!

我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

1 阅读:20

软件求生

简介:从事软件开发,分享“技术”、“运营”、“产品”等。