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

news2024/10/3 4:30:35

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

在这里插入图片描述

今天,我会介绍一些日常开发中类似线程死锁等问题的排查经验,并选择一两个我自己修复过或者诊断过的核心类库死锁问题作为例子,希望不仅能在面试时,包括在日常工作中也能对你有所帮助。

今天我要问你的问题是,什么情况下 Java 程序会产生死锁?如何定位、修复?

典型回答

死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

你可以利用下面的示例图理解基本的死锁问题:

在这里插入图片描述

定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。

如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。

考点分析

今天的问题偏向于实用场景,大部分死锁本身并不难定位,掌握基本思路和工具使用,理解线程相关的基本概念,比如各种线程状态和同步、锁、Latch 等并发工具,就已经足够解决大多数问题了。

针对死锁,面试官可以深入考察:

抛开字面上的概念,让面试者写一个可能死锁的程序,顺便也考察下基本的线程编程。

诊断死锁有哪些工具,如果是分布式环境,可能更关心能否用 API 实现吗?

后期诊断死锁还是挺痛苦的,经常加班,如何在编程中尽量避免一些典型场景的死锁,有其他工具辅助吗?

知识扩展

在分析开始之前,先以一个基本的死锁程序为例,我在这里只用了两个嵌套的 synchronized 去获取锁,具体如下:


public class DeadLockSample extends Thread {
  private String first;
  private String second;
  public DeadLockSample(String name, String first, String second) {
      super(name);
      this.first = first;
      this.second = second;
  }

  public  void run() {
      synchronized (first) {
          System.out.println(this.getName() + " obtained: " + first);
          try {
              Thread.sleep(1000L);
              synchronized (second) {
                  System.out.println(this.getName() + " obtained: " + second);
              }
          } catch (InterruptedException e) {
              // Do nothing
          }
      }
  }
  public static void main(String[] args) throws InterruptedException {
      String lockA = "lockA";
      String lockB = "lockB";
      DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
      DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
      t1.start();
      t2.start();
      t1.join();
      t2.join();
  }
}

这个程序编译执行后,几乎每次都可以重现死锁,请看下面截取的输出。另外,这里有个比较有意思的地方,为什么我先调用 Thread1 的 start,但是 Thread2 却先打印出来了呢?这就是因为线程调度依赖于(操作系统)调度器,虽然你可以通过优先级之类进行影响,但是具体情况是不确定的。

在这里插入图片描述

下面来模拟问题定位,我就选取最常见的 jstack,其他一些类似 JConsole 等图形化的工具,请自行查找。

首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。

其次,调用 jstack 获取线程栈:

${JAVA_HOME}\bin\jstack your_pid

然后,分析得到的输出,具体片段如下:

在这里插入图片描述

最后,结合代码分析线程栈信息。上面这个输出非常明显,找到处于 BLOCKED 状态的线程,按照试图获取(waiting)的锁 ID(请看我标记为相同颜色的数字)查找,很快就定位问题。 jstack 本身也会把类似的简单死锁抽取出来,直接打印出来。

在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为:

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

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

如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供了 findDeadlockedThreads() 方法用于定位。为方便说明,我修改了 DeadLockSample,请看下面的代码片段。


public static void main(String[] args) throws InterruptedException {

  ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  Runnable dlCheck = new Runnable() {

      @Override
      public void run() {
          long[] threadIds = mbean.findDeadlockedThreads();
          if (threadIds != null) {
                     ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
                     System.out.println("Detected deadlock threads:");
              for (ThreadInfo threadInfo : threadInfos) {
                  System.out.println(threadInfo.getThreadName());
              }
          }
       }
    };

       ScheduledExecutorService scheduler =Executors.newScheduledThreadPool(1);
       // 稍等5秒,然后每10秒进行一次死锁扫描
        scheduler.scheduleAtFixedRate(dlCheck, 5L, 10L, TimeUnit.SECONDS);
// 死锁样例代码…
}

重新编译执行,你就能看到死锁被定位到的输出。在实际应用中,就可以据此收集进一步的信息,然后进行预警等后续处理。但是要注意的是,对线程进行快照本身是一个相对重量级的操作,还是要慎重选择频度和时机。

如何在编程中尽量预防死锁呢?

首先,我们来总结一下前面例子中死锁的产生包含哪些基本元素。基本上死锁的发生是因为:

互斥条件,类似 Java 中 Monitor 都是独占的,要么是我用,要么是你用。

互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。

循环依赖关系,两个或者多个个体之间出现了锁的链条环。

所以,我们可以据此分析可能的避免死锁的思路和方法。

第一种方法

如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。否则,即使是非常精通并发编程的工程师,也难免会掉进坑里,嵌套的 synchronized 或者 lock 非常容易出问题。

我举个例子, Java NIO 的实现代码向来以锁多著称,一个原因是,其本身模型就非常复杂,某种程度上是不得不如此;另外是在设计时,考虑到既要支持阻塞模式,又要支持非阻塞模式。直接结果就是,一些基本操作如 connect,需要操作三个锁以上,在最近的一个 JDK 改进中,就发生了死锁现象。

我将其简化为下面的伪代码,问题是暴露在 HTTP/2 客户端中,这是个非常现代的反应式风格的 API,非常推荐学习使用。


/// Thread HttpClient-6-SelectorManager:
readLock.lock();
writeLock.lock();
// 持有readLock/writeLock,调用close()需要获得closeLock
close();
// Thread HttpClient-6-Worker-2 持有closeLock
implCloseSelectableChannel (); //想获得readLock

在 close 发生时, HttpClient-6-SelectorManager 线程持有 readLock/writeLock,试图获得 closeLock;与此同时,另一个 HttpClient-6-Worker-2 线程,持有 closeLock,试图获得 readLock,这就不可避免地进入了死锁。

这里比较难懂的地方在于,closeLock 的持有状态(就是我标记为绿色的部分)并没有在线程栈中显示出来,请参考我在下图中标记的部分。

在这里插入图片描述

更加具体来说,请查看SocketChannelImpl的 663 行,对比 implCloseSelectableChannel() 方法实现和AbstractInterruptibleChannel.close()在 109 行的代码,这里就不展示代码了。

所以,从程序设计的角度反思,如果我们赋予一段程序太多的职责,出现“既要…又要…”的情况时,可能就需要我们审视下设计思路或目的是否合理了。对于类库,因为其基础、共享的定位,比应用开发往往更加令人苦恼,需要仔细斟酌之间的平衡。

第二种方法

如果必须使用多个锁,尽量设计好锁的获取顺序,这个说起来简单,做起来可不容易,你可以参看著名的银行家算法。

一般的情况,我建议可以采取些简单的辅助手段,比如:

将对象(方法)和锁之间的关系,用图形化的方式表示分别抽取出来,以今天最初讲的死锁为例,因为是调用了同一个线程所以更加简单。

在这里插入图片描述

然后根据对象之间组合、调用的关系对比和组合,考虑可能调用时序。

在这里插入图片描述

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

在这里插入图片描述

第三种方法

使用带超时的方法,为程序带来更多可控性。

类似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。

并发 Lock 实现,如 ReentrantLock 还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法:


if (lock.tryLock() || lock.tryLock(timeout, unit)) {
    // ...
   }

第四种方法

业界也有一些其他方面的尝试,比如通过静态代码分析(如 FindBugs)去查找固定的模式,进而定位可能的死锁或者竞争情况。实践证明这种方法也有一定作用,请参考相关文档。

除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中,jstack 等工具也不见得能够显示全部锁信息,所以处理起来比较棘手。对此,Java 有官方文档进行了详细解释,并针对特定情况提供了相应 JVM 参数和基本原则。

今天,我从样例程序出发,介绍了死锁产生原因,并帮你熟悉了排查死锁基本工具的使用和典型思路,最后结合实例介绍了实际场景中的死锁分析方法与预防措施,希望对你有所帮助。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天的思考题是,有时候并不是阻塞导致的死锁,只是某个线程进入了死循环,导致其他线程一直等待,这种问题如何诊断呢?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

官方文档进行了详细解释,并针对特定情况提供了相应 JVM 参数和基本原则。

今天,我从样例程序出发,介绍了死锁产生原因,并帮你熟悉了排查死锁基本工具的使用和典型思路,最后结合实例介绍了实际场景中的死锁分析方法与预防措施,希望对你有所帮助。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天的思考题是,有时候并不是阻塞导致的死锁,只是某个线程进入了死循环,导致其他线程一直等待,这种问题如何诊断呢?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/380141.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NOC2021年测试卷1

一、单项选择题(共15题,每题3分,共45分) 1.下列应用不能体现人工智能技术的是()。 A、使用语音输入法输入文字B、使用OCR软件从图像中识别汉字C、某软件支持在线中英文互译D、某网站自动统计歌曲下载次数2.点击绿旗执行图中的脚本,若角色的坐标为 (25, 20) ,则说出的内…

元数据管理实践数据血缘

元数据管理实践&数据血缘 什么是元数据?元数据MetaData狭义的解释是用来描述数据的数据,广义的来看,除了业务逻辑直接读写处理的那些业务数据,所有其它用来维持整个系统运转所需的信息/数据都可以叫作元数据。比如…

微服务一 实用篇 - 2. SpringCloud02

《微服务一 实用篇 - 2. SpringCloud02》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《微服务一 实用篇 - 2. SpringCloud02》《微服务一 实用篇 - 2. SpringCloud02》1.Nacos配置管理1.1.统一配置管理1.1.1.在nacos中添加配置文件1.1.2.从微服务拉取…

GaN/氮化镓65W(1A2C)PD快充电源方案

近期美阔电子推出了一款全新的氮化镓65W(1A2C)PD快充充电器方案,该方案采用同系列控制单晶片:QR一次侧控制IC驱动MTCD-mode GaN FET(MGZ31N65-650V)、二次侧同步整流控制IC及PD3.0协议IC)可达到最佳匹配。 GaN/氮化镓作为第三代半…

IDEA中开发Android完整示例及APK的生成(包含用户名密码输入获取等操作)

先看看运行演示程序效果: 输入内容并点击确定按钮效果如下图, (已测试传到真机中安装运行效果一模一样兼容基本所有安卓版本:) 实现步骤如下: 1,先安装AndroidSDK 点击File->new project->Android 这个因为我…

TSBS 是什么?为什么时序数据库 TDengine 会选择它作为性能对比测试平台?

TSBS 是一个时序数据处理(数据库)系统的性能基准测试平台,提供了 IoT、DevOps 两个典型应用场景,它由 Timescale 开源并负责维护。作为一个性能基准测试平台,TSBS 具有便捷、易用、扩展灵活等特点,涵盖了时…

MySQL卸载教程(详细)

系列文章 C#项目–业务单据号生成器(定义规则、自动编号、流水号) 本文链接:https://blog.csdn.net/youcheng_ge/article/details/129129787 C#项目–开始日期结束日期范围计算(上周、本周、明年、前年等) 本文链接&…

taobao.open.account.update( Open Account数据更新 )

¥开放平台免费API不需用户授权 Open Account数据更新 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 响应参数 点击获取key和secret 请求示例 TaobaoClient client new DefaultTaobaoClient(url, appkey, sec…

2022 年江西省职业院校技能大赛高职组“信息安全管理与评估”赛项样题

2022 年江西省职业院校技能大赛高职组 “信息安全管理与评估”赛项样题 一、 赛项信息 第一场比赛: 竞赛阶段 任务阶 段 竞赛任务 竞赛时 间 分值 第一阶段 任务 1 网络平台搭建 90 平台搭建与安全 任务 2 网络安全设备配置与防护 210 设备配置防护…

【调试方法】基于vs环境下的实用调试技巧

前言: 对万千程序猿来说,在这个世界上如果有比写程序更痛苦的事情,那一定是亲手找出自己编写的程序中的bug(漏洞)。作为新手在我们日常写代码中,经常会出现报错的情况(好的程序员只是比我们见过…

4.排序算法之一:冒泡排序

排序算法稳定性假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]r[j],且r[i]在r[j]之前,而在排序后的序列中,r[…

操作系统权限提升(二十一)之Linux提权-环境变量劫持提权

系列文章 操作系统权限提升(十八)之Linux提权-内核提权 操作系统权限提升(十九)之Linux提权-SUID提权 操作系统权限提升(二十)之Linux提权-计划任务提权 环境变量劫持提权 环境变量劫持提权原理 PATH是Linux和类Unix操作系统中的环境变量,类似windows中的path环…

蓝海彤翔执行副总裁张加廷接受【联播苏州】独家专访

今年春节档,科幻类电影《流浪地球2》票房口碑双丰收,截至目前,累计票房已破 38 亿,淘票票评分 9.6 ,影片的特效质感可以媲美国际顶尖水平。其中,蓝海彤翔为影片的后期制作提供了出色的渲染服务。2月21日&am…

前端学习第二阶段-第4章 移动web开发

4-1 媒体查询 01-移动WEB开发rem适配布局导读 02-rem单位 03-媒体查询语法简介 04-媒体查询案例背景变色 05-媒体查询rem实现元素动态大小变化 06-媒体查询引入资源 4-2 less介绍和使用 07-CSS的弊端 08-less简介以及安装 09-less变量 10-less编译easy less插件 11-less嵌套 12…

Linux命令-mdadm管理磁盘阵列组

文章目录​​​​​​​ 概要 一 磁盘阵列是什么? 二 RAID的级别 RAID 0 RAID 1 RAID 5 RAID10 三 命令介绍 四 语法格式 五 基本参数 六 参考实例 创建RAID 0磁盘阵列组 创建RAID 1磁盘阵列组 创建RAID 5磁盘阵列组 创建RAID 10磁盘阵列组…

【Flutter入门到进阶】Flutter基础篇---第一个Flutter应用

1 Flutter目录结构介绍 1.1 创建项目 flutter create flutterdemo 1.2 目录结构 1.3 结构说明 1、android、ios、linux、macos、web、windows文件夹:都是对应平台相关代码 2、lib文件夹:flutter相关代码,我们编写的代码就在这个文件夹 3、t…

八、异步编程

文章目录异步编程FutureTask应用&源码分析FutureTask介绍FutureTask应用FutureTask源码分析FutureTask中的核心属性FutureTask的run方法FutureTask的set&setException方法FutureTask的cancel方法FutureTask的get方法FutureTask的finishCompletion方法CompletableFuture…

DevOps 学习笔记(一) | DevOps 简介及环境搭建

1. 环境配置 本次实验需要三台服务器CI/CD 服务器、应用服务器和Harbor 服务器 DevOps 步骤 程序员将代码 push 到代码仓库Jenkins 根据触发条件拉取代码到CI/CD 服务器Jenkins 使用 Maven 将代码 build 成 jar 包Jenkins 使用 jar 包通过 Dockerfile 和 docker-compose.yml…

HBase JMX 指标学习

名词解释: JMX:Java Management Extensions,用于用于Java程序扩展监控和管理项。 GC:Garbage Collection,垃圾收集,垃圾回收机制。 1、概述 说到对Hadoop和 HBase的集群监控,大家知道的和用…

YOLOv8详解 【网络结构+代码+实操】

文章目录YOLOv8 概述模型结构Loss 计算训练数据增强训练策略模型推理过程网络模型解析卷积神经单元(model.py)Yolov8实操快速入门环境配置数据集准备模型的训练/验证/预测/导出使用CLI使用python多任务支持检测实例分割分类配置设置操作类型训练预测验证…