问题描述

当我们一个系统既需要mysql驱动,也需要oracle驱动的时候,在并发加载初始化这些驱动类的过程中产生死锁的可能性非常大,下面是一个模拟的例子,对于Thread2的实现其实是jdk里java.sql.DriverService的逻辑,也是我们第一次调用java.sql.DriverManager.registerDriver注册一个驱动实例要走的逻辑(jdk1.6下),不过这篇文章是使用我们生产环境的一个系统的线程dump和内存dump为基础进行分析展开的。

如果以上代码运行过程中发现有线程一直卡死在Class.forName的调用里,那么说明问题已经重现了。

先上两张图

内存态线程堆栈

线程堆栈

存疑点

仔细看看上面的线程dump分析和内存dump分析里的线程分析模块,您可能会有如下两个疑惑:

  • 【为什么线程[Thread-0]一直卡在Class.forName的位置】:这有点出乎意料,做一个类加载要么找不到抛出ClassNotFoundException,要么找到直接返回,为什么会一直卡在这个位置呢?

  • 【明明[Thread-0]注册的是mysql驱动为什么会去加载Odbc的驱动类】:通过[Thread-0]在栈上看倒数第二帧展开看到传入Class.forName的参数是com.mysql.jdbc.Driver,然后展开栈上顺序第二帧,看到传入的参数是sun.jdbc.odbc.JdbcOdbcDriver,这意味着在对mysql驱动类做加载初始化的过程中又触发了JdbcOdbc驱动类的加载

疑惑点解释

疑惑二:

第一个疑惑我们先留着,先解释下第二个疑惑,大家可以对照堆栈通过反编译rt.jar还有ojdbc6-11.2.0.3.0.jar看具体的代码

驱动类加载过程简要介绍:

当要注册某个sql驱动的时候是通过调用java.sql.DriverManager.registerDriver来实现的(注意这个方法加了synchronized关键字,后面解释第一个疑惑的时候是关键),而这个方法在第一次执行过程中,会在当前线程classloader的classpath下寻找所有/META-INF/services/java.sql.Driver文件,这个文件在mysql和oracle驱动jar里都有,里面写的是对应的驱动实现类名,这种机制是jdk提供的spi实现,找到这些文件之后,依次使用Class.forName(driverClassName, true, this.loader)来对这些驱动类进行加载,其中第二个参数是true,意味着不仅仅做一次loadClass的动作,还会初始化该类,即调用包含静态块的< clinit >方法,执行完之后才会返回,这样就解释了第二个疑惑,在mysql驱动注册过程中还会对odbc驱动类进行加载并初始化

感想:
其实我觉得这种设计有点傻,为什么要干和自己不相关的事情呢,画蛇添足的设计,首先类初始化的开销是否放到一起做并没有多大区别,其次正由于这种设计导致了今天这个死锁的发生

疑惑一:

现在来说第一个疑惑,为什么会一直卡在Class.forName呢,到底卡在哪里,于是再通过jstack -m 命令将jvm里的堆栈也打印出来,如下所示

我们看到其实正在做类的初始化动作,并且线程正在调用ObjectSynchronizer::waitUninterruptibly一直没返回,在看这方法的调用者instanceKlass1::initialize_impl,我们找到源码位置如下:

类的初始化过程:

当某个线程获得机会对某个类进行初始化的时候(请看上面的Step 6),会设置这个类的init_state属性为being_initialized(如果初始化好了会设置为fully_initialized,异常的话会设置为initialization_error),还会设置init_thread属性为当前线程,在这个设置过程中是有针对这个类提供了一把互斥锁的,因此当有别的线程进来的时候会被拦截在外面,如果设置完了,这把互斥锁也释放了,但是因为这个类的状态被设置了,因此并发问题也得到了解决,当另外一个线程也尝试初始化这个类的时候会判断这个类的状态是不是being_initialized,并且其init_thread不是当前线程,那么就会一直卡在那里,也就是此次线程dump的线程所处的状态,正在初始化类的线程会调用< clinit >方法,如果正常结束了,那么就设置其状态为fully_initialized,并且通知之前卡在那里等待初始化完成的线程,然他们继续往下走(下一个动作就是再判断下状态,发现完成了就直接return了)

猜想:

在了解了上面的过程之后,于是我们猜测两种可能

  • 第一,这个类的状态还是being_intialized,还在while循环里没有跳出来

  • 第二,事件通知机制出现了问题,也就是pthread_cond_wait和pthread_cond_signal之间的通信过程出现了问题。

不过第二种可能性非常小,比较linux久经考验了,那接下来我们验证其实是第一个猜想

验证:

我们通过GDB attach的方式连到了问题机器上(好在机器没有挂),首先我们要找到具体的问题线程,我们通过上面的jstack -m命令看到了线程ID是5738,然后通过info threads找到对应的线程,并得到它的序号14

然后通过thread 14切换到对应的线程,并通过bt看到了如下的堆栈,正如我们想象的那样,正在做类的初始化,一直卡在那里

我们通过f 6选择第7帧,在通过disassemble反汇编该帧,也就是对instanceKlass::initialize_impl ()这个方法反汇编

从上面的注释我们其实得出了,我们要看当前类的初始化状态,那就是看eax寄存器偏移0xe0的位置的值,而eax其实就是ebp寄存器偏移0xfffffff4位置的值,于是我们通过如下地址内存查到得到是4

而4其实代表的就是being_initialized这个状态,代码如下

从这于是我们验证了第一个猜想,其实是状态一直没有变更,因此一直卡在那里,为了更进一步确认这个问题,要是我们能找到该类的init_thread线程id就更清楚了,拿到这个ID我们就能看到这个线程栈,就知道它在干什么了,但是很遗憾,这个很难获取到,至少我一直没有找到办法,因为线程ID在线程对象里一直没有存,都是调用的os函数来获取的,得换个思路。

突然发现instanceKlass.hpp代码中得知两个属性原来是相邻的(init_state和init_thread),于是断定下一个地址的值就代表是这个线程对象了,但是其属性何其多,找到想要的太不易了,最主要的是还担心自己看的代码和服务器上的jvm代码不一致,这样更蛋疼了,于是继续查看Thread.hpp中的JavaThread类,找到个关键字0xDEAD-2=0xDEAB,这个有可能是volatile TerminatedTypes _terminated属性的值,于是把线程对象打印出来,果然查到了关键字0xDEAB

因此顺着这个属性继续往上找,找到了_thread_state表示线程状态的值(向上偏移三个字),0x0000000a,即10,然后查看代码知道原来线程是出于block状态

JavaThreadState

这样一来查看下线程dump,发现Thread-1正好处于BLOCKED状态,也就是说Thread-1就是那个正在对mysql驱动类做初始化的线程,这说明Thread-0和Thread-1成功互锁了

于是我们展开Thread-1,看到- waiting to lock <0x71ae2ec0> (a java.lang.Class for java.sql.DriverManager),该线程正在等待java.sql.DriverManager类型锁,而blocked在那里,而这个类型锁是被Thread-0线程持有的,从Thread-1这个线程堆栈来看它其实也是在做Class.forName动作,并且通过Thread-1,展开第四帧我们可以看到其正在对加载sun.jdbc.odbc.JdbcOdbcDriver

问题现场遐想:

于是我们大胆设想一个场景,Thread-1先获取到初始化sun.jdbc.odbc.JdbcOdbcDriver的机会,然后在执行sun.jdbc.odbc.JdbcOdbcDriver这个类的静态块的时候调用DriverManager.registerDriver(new Driver());,而该方法之前已经提到了是会加同步锁的,再想象一下,在这个这个静态块之前,并且设置了sun.jdbc.odbc.JdbcOdbcDriver类的初始化状态为being_initialized之后,Thread-0这个线程执行到了卡在的那个位置,并且我们从其堆栈可以看出它已经持有了java.sql.DriverManager这个类型的锁,因此这两个线程陷入了互锁状态

解决方案

解决方案目前想到的是将驱动类的加载过程变成单线程加载,不存在并发情况就没问题了

推荐阅读:

如此火爆的ZooKeeper,到底如何选主?

服务刚启动就 Old GC,要闹哪样?

JDK的sql设计不合理导致的驱动类初始化死锁问题的更多相关文章

  1. “秒杀”问题的数据库和SQL设计【转载】

    “秒杀”问题的数据库和SQL设计 APRIL 21ST, 2015 问题的来源 完全不考虑一致性的方案 表结构 方案 存在的问题 保证单用户不会重复购买 解决超卖问题 方案 优化 提高性能了 鱼与熊掌 ...

  2. SQL Server 2008过期导致MSSQLSERVER服务无法启动现象

    SQL Server 2008过期导致MSSQLSERVER服务无法启动现象:安装的是SQL Server 2008评估版,180天的试用期后,MSSQLSERVER服务就无法启动,手动启动就报告17 ...

  3. rac数据库默认sql tuning advisor,导致大量library cache lock

    rac数据库默认sql tuning advisor,导致大量library cache lock 问题现象:客户反映周六周日固定十点钟,一个程序会特别慢(大概10分钟),平时1到2秒.查看当时的日志 ...

  4. 设计、定义并实现Complex类

    设计.定义并实现Complex类 #include <iostream> #include <cmath> using namespace std; class MyCompl ...

  5. srping mvc 集成CXF 导致类初始化两遍

    cxf依赖于spring的ContextLoaderListener,而spring mvc 则依赖于DispatcherServlet. 初始化DispatcherServlet的时候会依赖初始化一 ...

  6. 设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”。。。

    设计一个 Java 程序,自定义异常类,从命令行(键盘)输入一个字符串,如果该字符串值为“XYZ”,则抛出一个异常信息“This is a XYZ”,如果从命令行输入 ABC,则没有抛出异常.(只有 ...

  7. PHP 进阶篇:面向对象的设计原则,自动加载类,类型提示,traits,命名空间,spl的使用,反射的使用,php常用设计模式 (麦子学员 第三阶段)

    以下是进阶篇的内容:面向对象的设计原则,自动加载类,类型提示,traits,命名空间,spl的使用,反射的使用,php常用设计模式 ================================== ...

  8. 在sql设计中没法修改表结构

    在做练习的时候经常表没设计好,后来有要去数据库修改表结构但是没词用界面修改的时候都会提示要保存 转自http://www.57xue.com/ItemView/Sql/2016061600160.ht ...

  9. PlSqlDev中执行INSERT SQL语句包含&符号导致数据异常

    在PLSQL Developer中执行Insert语句时提示如下信息: 当时未注意,直接点击OK按钮继续. 导入数据后查看发现部分数据中的参数丢失了一部分,呈以下规律: . 而正常应为: . 经询问大 ...

随机推荐

  1. Nginx 配置GeoIP2 禁止访问,并允许添加白名单过滤访问设置

    配置环境:Centos 7.6 + Tengine 2.3.2 GeoIP2 下载地址:https://dev.maxmind.com/geoip/geoip2/geolite2/ 1. Nginx  ...

  2. 浏览器渲染流程&Composite(渲染层合并)简单总结

    梳理浏览器渲染流程 首先简单了解一下浏览器请求.加载.渲染一个页面的大致过程: DNS 查询 TCP 连接 HTTP 请求即响应 服务器响应 客户端渲染 这里主要将客户端渲染展开梳理一下,从浏览器器内 ...

  3. Asp.Net Core IdentityServer4 中的基本概念

    一.前言 这篇文章可能大家会觉得很空洞,没有实际的实战东西,主要是自己整理出来的IdentityServer4 的一些概念性的东西:如果你对IdentityServer4有过一定的实战经验,可以跳过不 ...

  4. .NET微服务从0到1:服务注册与发现(Consul)

    目录 Consul搭建 基于Docker搭建Consul 基于Windows搭建Consul ServiceA集成Consul做服务注册 Ocelot集成Consul做服务发现 更多参考 Consul ...

  5. Python - 字符串格式化详解(%、format)

    Python在字符串格式化的两种方式 % format %,关于整数的输出 %o:oct 八进制%d:dec 十进制%x:hex 十六进制 print("整数:%d,%d,%d" ...

  6. 关于在layui中的table checkbox 默认选中设置

    一.layui版本 layui-v2.4.5 二.设置table的checkbox默认选中 总共有两种方法: 方法1:在返回的json中设置LAY_CHECKED为true,页面上的checkbox就 ...

  7. IRM3800 红外遥控器解码 linux驱动

    这一次还是接在 Cemera 上.用 中断引脚 EINT20 也就是 GPG12. 之前焊的 51 板子上有一个红外接收器. 请注意了,是 标准的 NEC 码规范:首次发送的是9ms的高电平脉冲,其后 ...

  8. 通过欧拉计划学Rust编程(第500题)

    由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾. "欧拉计划"的网址: https://projecteuler.net 英文如果不过关 ...

  9. 上线前测试的bug,要不要处理,跟版本的关系

    最近有两个项目是在旧版本上实施的.上线前经过一轮测试后,发现了一些产品(我们的产品确实不稳定) 在这个项目上,修改产品bug是肯定的.但是要不要追踪这些bug? 这就跟版本使用范围有关系了,毕竟要考虑 ...

  10. background-attachment 制造视差滚动效果案例

    简介 background-attachment 属性设置背景图像是否固定或者随着页面的其余部分滚动.可能的值有三个: scroll 默认值.背景图像会随着页面其余部分的滚动而移动. fixed 当页 ...