写点什么

并发王者课 - 黄金 1:两败俱伤 - 互不相让的线程如何导致了死锁僵局

用户头像
秦二爷
关注
发布于: 2021 年 06 月 12 日

欢迎来到《并发王者课》,本文是该系列文章中的第 11 篇


在本篇文章中,我将为你介绍多线程中的经典问题-死锁,以及死锁的产生原因、处理和方式预防措施

一、死锁的产生

观察下面这幅图,线程 1 持有了 A,但它需要 B;而线程 2 持有了 B,但是它需要 A。


你看,问题就来了,A、B 都在等待对方已经持有的资源,并且都不释放,这就让事情陷入了僵局,也就是产生了死锁



在并发编程中,死锁表示的是一种状态。在这种状态下,各方都在等待另一方释放所持有的资源,但是它们之间又缺乏必要的通信机制,导致彼此存在环路依赖而永远地等待下去。


死锁不仅存在于 Java 程序中,在诸如数据库等其他中间件及分布式架构中都会存在。在数据的设计中,会考虑到死锁的监测和恢复。当数据库中发生死锁时,将选择一个牺牲者并放弃对应的事务,同时释放锁定的资源。在它的竞争者执行结束后,应用程序可以重新运行这个事务,因为它的竞争者此前已经完成事务。


然而,在 JVM 中,处理死锁并没有数据库中那么优雅。当一组线程发生死锁时,“游戏”将到此结束,这些线程将不能再使用,而这可能会直接导致应用程序崩溃、性能降低或者部分功能停止


所以,和其他并发问题一样,死锁是危险的,死锁造成的影响会立即表现出来,而如果在高负载情况下,这将是一场灾难

二、死锁产生的必要条件

从第一小节的图示中,我们可以看到死锁产生的一些必要条件:


  1. 互斥:一个资源每次只能被一个线程使用。比如,上图中的 A 和 B 同时只能被线程 1 和线程 2 其中一个使用;

  2. 请求与保持条件:一个线程在请求其他资源被阻塞时,对已经持有的资源保持不释放。比如,上图中的线程 1 在请求 B 时,并不会释放 A;

  3. 不剥夺条件:对于线程已经获得的资源,在它主动释放前,不可以主动剥夺。比如,上图中线程 1 和线程 2 已经获得的资源,除非自己释放,否则不可以被强制剥夺;

  4. 循环等待条件:多个线程之间形成环状等待。上图中的线程 1 和线程 2 所形成的就是循环等待。

三、模拟并体验死锁

在了解什么是死锁及其产生的条件后,我们根据上图中的死锁情景,通过一段代码来模拟体验死锁的发生。


根据上图所示,定义哪吒线程,在运行时将持有 A 并请求 B


private static class NeZha implements Runnable {  public void run() {    synchronized(lockA) {      System.out.println("哪吒: 持有A!");
try { Thread.sleep(10); } catch (InterruptedException ignored) {} System.out.println("哪吒: 等待B...");
synchronized(lockB) { System.out.println("哪吒: 已经同时持有A和B..."); } } }}
复制代码


定义兰陵王线程,在运行时持有 B 并请求 A


private static class LanLingWang implements Runnable {  public void run() {    synchronized(lockB) {      System.out.println("兰陵王: 持有B!");
try { Thread.sleep(10); } catch (InterruptedException ignored) {} System.out.println("兰陵王: 等待A...");
synchronized(lockA) { System.out.println("兰陵王: 已经同时持有A和B..."); } } }}
复制代码


启动两个线程:


public class DeadLockDemo {    public static final Object lockA = new Object();    public static final Object lockB = new Object();
public static void main(String args[]) { Thread thread1 = new Thread(new NeZha()); Thread thread2 = new Thread(new LanLingWang()); thread1.start(); thread2.start(); }}
复制代码


两个线程的输出结果如下:


哪吒: 持有A!兰陵王: 持有B!哪吒: 等待B...兰陵王: 等待A...
复制代码


从结果中可以看到,哪吒和兰陵王分别持有了 A 和 B,但他们又相互请求对方持有的资源,最终导致死锁,两个线程进入了无限地等待

四、死锁的处理

1. 忽略死锁

忽略死锁是一种鸵鸟政策,它假设永远不会发生死锁。这种策略适用于死锁发生概率较低且影响可容忍的场景,如果死锁被证明永远不会发生也可以采用这种策略

2. 检测

在这种策略下,死锁是允许发生的。如果系统检测到死锁,也会对其进行纠正,比如跟踪线程状态和资源分配。在死锁时,可以通过一些方法进行纠正:


  • 线程终止:选择其中一个或多个线程进行终止,释放资源,打破死锁状态;

  • 资源抢占:重新分配各线程已经抢占的资源,直到打破死锁。

3. 预防

对待死锁问题,预防是关键。本文第二小节已经列举死锁产生的一些必要条件,所以如果要预防死锁,只要打破其中任一条件即可,Java 中具体的死锁预发方式我们会在后面的文章中介绍。

小结

以上就是关于死锁的全部内容。在本文中,我们介绍了什么是死锁,以及死锁产生的必要条件和应对策略。对待开发中的死锁问题,既要保持敬畏之心,也不必闻之色变,审慎分析死锁的可能并设计合理策略可以有效预防死锁。


正文到此结束,恭喜你又上了一颗星✨


夫子的试炼


  • 运行本文的示例代码,尝试找到破解其死锁的方法。


延伸阅读与参考资料



关于作者


关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。


如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

发布于: 2021 年 06 月 12 日阅读数: 297
用户头像

秦二爷

关注

微信公众号:【庸人技术笑谈】 2018.05.13 加入

记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。

评论

发布
暂无评论
并发王者课-黄金1:两败俱伤-互不相让的线程如何导致了死锁僵局