什么情况下Java程序会产生死锁?如何定位、修复? - Go语言中文社区

什么情况下Java程序会产生死锁?如何定位、修复?


或许我们经常会在面试中或者听别人说,老师讲课也都会经常听见死锁,并且在实际并发的线程中很容易遇到这种情况,那么究竟什么是死锁,死锁如何产生的,又如何修复呢?不仅仅是为了面试,在实际开发中,也是十分重要的知识点。

简述

死锁是一种特定的,在程序中的一种状态,在我们的程序中,由于循环依赖,导致彼此一直处于等待之中,没有任何个体可以继续前进,停滞住了,就会产生死锁。但是,死锁也不仅仅是会在线程中产生的现象,在资源独占的进程之间,也同样会产生死锁的现象。但是通常来讲,我们更加关注多线程场景产生的死锁,意思很简单,两个或者多个线程持有对方需要的锁,而永久处于阻塞状态。

在这里插入图片描述

而找到死锁具体位置的方法,是使用jstack等工具获取到线程栈,然后确定之间的依赖关系,进而找到死锁的位置。如果死锁的位置做够明显,我们使用jstack或者jconsole就可以直接在图形界面进行有限的死锁检测。

但是如果程序运行的时候发生了死锁,基本无法在线解决,只能stop进程去修复。所以,代码开发阶段要互相审查,或者利用工具手动排查。

从面试角度来说,面试官经常会问:

  1. 手写一个可能死锁的程序
  2. 诊断死锁有哪些工具,分布式环境是否有相关的工具
  3. 如何避免死锁

扩展

在分析开始之前,先手写一个会死锁的程序,在这里只用两个嵌套的synchronized去获取锁,具体如下:
在这里插入图片描述
我们可以发现 ,这里先输出了下面的Thread2,然后才输出Thread1,这是因为程序里的线程调度依赖于操作系统,虽然你可以通过优先级之类进行影响但是具体情况不好说。

然后我们用jstack捕捉到线程栈,得出结果如下(来源于极客时间):

在这里插入图片描述

然后分析我们的线程栈信息,找到处于Blocked状态的进程,按照视图获取的锁去定位,就可以很快定位到死锁的位置。但类死锁的情况未必会显示得这么清楚,但是从整体上可以理解为:

区分线程状态 -> 查看等待目标 -> 对比Monitor等持有信息

所以,理解线程基本状态和并发相关元素是定位问题的关键,然后配合程序调用栈,基本就可以定位的问题代码。

同时我们也可以自己设计寻找死锁位置的程序,java官方也提供了相关的API,ThreadMXBean,其直接提供了findDeadlockedThreads()方法用于定位。修改以上代码如下:

在这里插入图片描述

但是这种修改方法对于线程本身是一个相对重量级的操作,所以慎用,找bug用还是可以的。

那么如何在编程中尽量预防死锁呢?
首先,我们来总结一下前面产生死锁的原因。

  • 互斥条件,就比如说Java中的Monitor只能被一个线程所使用。
  • 循环依赖,两个或者多个个体之间出现了锁的链条环

所以我们分析出避免死锁的思路和方法如下:

  1. 第一种方法

尽量避免使用多种锁,并且只有有需要的时候才用锁,尽量不适用嵌套的synchronized或者lock。

举个例子:java.nio包下以锁多而著称,尤其是NIO2,在设计的时候考虑到又要顾及非阻塞模式,又要顾及阻塞模式,所以本身模型十分复杂。导致的结果就是,一个简单的api背后就是好几把锁来控制,所以非常容易死锁。

截取极客时间的两张图:
在这里插入图片描述
在这里插入图片描述

从程序设计的调度来说,我们往往考虑到“既要…又要…”的功能设计的时候,往往会涉及到使用锁,那么设计思路或者目的就很重要了,因为其基础,共享的定位也是十分重要的,这比应用开发更加重要,需要仔细斟酌平衡。

  1. 第二种方法

如果必须要使用多种锁了,尽量设计好锁的获取顺序,可以参照银行家算法

银行家算法:银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。

一般情况下,可以采用简单的辅助手段:

  • 将方法和锁之间的关系,用图形化的方式抽取出来,因为调用了同一个线程,所以更加简单。
                                               在这里插入图片描述
  • 然后根据对象之间组合、调用的关系和对比组合,考虑可能调用时序。

在这里插入图片描述

  • 按照可能时序合并,发现可能死锁的场景。

在这里插入图片描述

  1. 第三种办法

使用.wait,为线程带来更多可控性。4await()都支持所谓的time wait,我们完全可以假定该锁不一定获得,指定超时时间,并为无法获得锁的时候退出逻辑。

并发Lock实现,如之前专栏提到的ReentrantLock还支持非阻塞的锁获取操作tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果wait的时候资源没有被占用,就会直接抢过来用。

使用方法如下:

if (lock.tryLock() || lock.tryLock(timeout, unit)) {  // ...  }
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_41936805/article/details/95104574
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢